Skip to main content
The Stripe integration powers all payment functionality in Bounty, from receiving bounty funding to disbursing payments to contributors.

Overview

Bounty uses Stripe for:
  • Checkout Sessions: Accepting bounty funding payments
  • Payment Intents: Holding funds until bounty completion
  • Stripe Connect: Enabling solvers to receive payouts
  • Transfers: Sending money to solver Connect accounts
  • Webhooks: Real-time payment status updates

Prerequisites

  • Stripe account with Connect enabled
  • Access to Stripe Dashboard
  • HTTPS-enabled domain (required for webhooks in production)

Stripe Setup

1

Create Stripe Account

Sign up at stripe.com if you don’t have an account.Activate your account by providing business information.
2

Enable Stripe Connect

Navigate to Stripe Dashboard → Settings → Connect.Click Get Started to enable Connect for your account.
Stripe Connect must be enabled for solver payouts to work. Without it, bounty creators can fund bounties, but solvers cannot receive payments.
3

Get API Keys

Go to Developers → API Keys.Copy your:
  • Publishable key (starts with pk_test_ or pk_live_)
  • Secret key (starts with sk_test_ or sk_live_)
4

Configure Webhooks

Navigate to Developers → Webhooks.Click Add endpoint and configure:
  • Endpoint URL: https://your-domain.com/api/webhooks/stripe
  • Events to send: Select the following:
    • checkout.session.completed
    • payment_intent.succeeded
    • payment_intent.payment_failed
    • payment_intent.canceled
    • charge.refunded
    • charge.refund.updated
    • transfer.created
    • transfer.updated
    • account.updated
After creating the endpoint, copy the Signing secret (starts with whsec_).
5

Set Environment Variables

Add to your .env file:
# Stripe API Keys
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_PUBLISHABLE_KEY="pk_test_..."
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."

# Stripe Connect Webhook
STRIPE_CONNECT_WEBHOOK_SECRET="whsec_..."
Use test mode keys (sk_test_, pk_test_) during development. Switch to live keys (sk_live_, pk_live_) for production.

Payment Flow

Bounty’s payment flow ensures funds are held securely until work is completed.

1. Bounty Creation

When a bounty creator creates a bounty:
// User creates bounty with amount
const bounty = await createBounty({
  title: 'Fix authentication bug',
  amount: 500,
  currency: 'USD',
});

// Bounty starts in 'draft' status with 'pending' payment status

2. Checkout Session

To fund the bounty, a Stripe Checkout session is created:
const session = await stripe.checkout.sessions.create({
  payment_method_types: ['card'],
  line_items: [{
    price_data: {
      currency: 'usd',
      product_data: {
        name: bounty.title,
      },
      unit_amount: bounty.amount * 100, // Convert to cents
    },
    quantity: 1,
  }],
  mode: 'payment',
  success_url: `${baseUrl}/bounty/${bounty.id}?funded=true`,
  cancel_url: `${baseUrl}/bounty/${bounty.id}`,
  metadata: {
    bountyId: bounty.id,
  },
});

3. Payment Processing

When the user completes checkout:
  1. Stripe processes the payment
  2. checkout.session.completed webhook fires
  3. Bounty status updates to open with payment status held
  4. Funds are held in your Stripe account (not yet transferred)

4. Payout to Solver

When a bounty is completed and awarded:
// Transfer funds to solver's Connect account
const transfer = await stripe.transfers.create({
  amount: amountInCents,
  currency: 'usd',
  destination: solver.stripeConnectAccountId,
  metadata: {
    bountyId: bounty.id,
  },
});

// Update bounty status to 'completed'
// Create payout record in database

Stripe Connect for Solvers

Solvers must connect a Stripe account to receive payouts.

Onboarding Flow

1

Create Connect Account

When a solver wants to receive payments, create a Connect account:
const account = await stripe.accounts.create({
  type: 'express',  // Express account for simplified onboarding
  email: user.email,
  capabilities: {
    transfers: { requested: true },
  },
  metadata: {
    userId: user.id,
  },
});
2

Generate Account Link

Create an onboarding link for the user:
const accountLink = await stripe.accountLinks.create({
  account: account.id,
  refresh_url: `${baseUrl}/settings/payments?onboarding=refresh`,
  return_url: `${baseUrl}/settings/payments?onboarding=success`,
  type: 'account_onboarding',
});

// Redirect user to accountLink.url
3

Complete Onboarding

The user completes Stripe’s onboarding form:
  • Personal information
  • Bank account details
  • Tax information (if required)
4

Verify Account Status

Check if onboarding is complete:
const account = await stripe.accounts.retrieve(accountId);

const onboardingComplete = 
  account.details_submitted &&
  account.charges_enabled &&
  account.payouts_enabled;

API Endpoints

connect.getConnectStatus

Check the user’s Connect account status:
const { data } = await trpc.connect.getConnectStatus.query();

if (data.hasConnectAccount && data.onboardingComplete) {
  // User can receive payouts
} else {
  // Redirect to onboarding
}
Generate an onboarding or re-onboarding link:
const { data } = await trpc.connect.createConnectAccountLink.mutate();

// Redirect user to data.url
window.location.href = data.url;
Generate a login link to Stripe Express Dashboard:
const { data } = await trpc.connect.getConnectDashboardLink.mutate();

if (data.isOnboarding) {
  // User needs to complete onboarding
} else {
  // Redirect to Express Dashboard
  window.location.href = data.url;
}

Webhook Handling

Bounty listens for Stripe webhook events to update bounty and payment status.

Webhook Security

All webhooks are verified using the signing secret:
import { constructEvent } from '@bounty/stripe';

const signature = request.headers['stripe-signature'];
const event = constructEvent(
  body,
  signature,
  env.STRIPE_CONNECT_WEBHOOK_SECRET
);
Always verify webhook signatures to prevent unauthorized requests from spoofing payment events.

Key Webhook Events

checkout.session.completed

Fired when a checkout session is completed:
const session = event.data.object;
const bountyId = session.metadata?.bountyId;
const paymentIntentId = session.payment_intent;

// Update bounty status to 'open' and payment status to 'held'
await db.update(bounty)
  .set({
    paymentStatus: 'held',
    status: 'open',
    stripePaymentIntentId: paymentIntentId,
  })
  .where(eq(bounty.id, bountyId));

payment_intent.succeeded

Fired when a payment is captured:
const paymentIntent = event.data.object;
const bountyId = paymentIntent.metadata?.bountyId;

// Atomically update bounty status (prevents race conditions)
await db.update(bounty)
  .set({
    paymentStatus: 'held',
    status: 'open',
  })
  .where(
    and(
      eq(bounty.id, bountyId),
      sql`${bounty.paymentStatus} != 'held'`
    )
  );

// Create transaction record
await db.insert(transaction).values({
  bountyId,
  type: 'payment_intent',
  amount: paymentIntent.amount / 100,
  stripeId: paymentIntent.id,
});

charge.refunded

Fired when a payment is refunded:
const charge = event.data.object;
const paymentIntentId = charge.payment_intent;

// Find bounty by payment intent
const bountyRecord = await db
  .select()
  .from(bounty)
  .where(eq(bounty.stripePaymentIntentId, paymentIntentId))
  .limit(1);

// Update to cancelled/refunded
await db.update(bounty)
  .set({
    status: 'cancelled',
    paymentStatus: 'refunded',
  })
  .where(eq(bounty.id, bountyRecord.id));

// Send confirmation email
await sendRefundEmail(bountyRecord);

transfer.created

Fired when a transfer to a Connect account is created:
const transfer = event.data.object;
const bountyId = transfer.metadata?.bountyId;

// Log transfer transaction
await db.insert(transaction).values({
  bountyId,
  type: 'transfer',
  amount: (transfer.amount / 100).toFixed(2),
  stripeId: transfer.id,
});

Testing

Test Mode

Use Stripe test mode for development:
  1. Use test API keys (sk_test_, pk_test_)
  2. Use test card numbers:
    • Success: 4242 4242 4242 4242
    • Decline: 4000 0000 0000 0002
    • Requires authentication: 4000 0025 0000 3155

Webhook Testing

Use Stripe CLI to forward webhooks to localhost:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Trigger test events:
stripe trigger checkout.session.completed
stripe trigger payment_intent.succeeded

Troubleshooting

”STRIPE_SECRET_KEY is required”

Cause: Environment variable not set. Solution: Ensure .env file contains:
STRIPE_SECRET_KEY="sk_test_..."

“Stripe Connect is not enabled”

Cause: Connect not activated in Stripe Dashboard. Solution:
  1. Go to Stripe Dashboard → Connect
  2. Click Get Started and complete setup

Webhook Signature Verification Failed

Cause: STRIPE_CONNECT_WEBHOOK_SECRET doesn’t match webhook endpoint. Solution:
  1. Go to Webhooks
  2. Click on your endpoint
  3. Copy the Signing secret (whsec_...)
  4. Update STRIPE_CONNECT_WEBHOOK_SECRET in .env

Bounty Not Updating After Payment

Checklist:
  • Verify webhook endpoint is accessible (HTTPS in production)
  • Check webhook delivery logs in Stripe Dashboard
  • Ensure bountyId is in payment metadata
  • Review server logs for webhook processing errors

Transfer Failed: “No such destination”

Cause: Solver’s Connect account ID is invalid or deleted. Solution:
  1. Check user.stripeConnectAccountId exists
  2. Verify account exists: stripe.accounts.retrieve(accountId)
  3. Ensure solver completed onboarding

API Reference

connect.getConnectStatus

Returns the user’s Stripe Connect account status. Returns: { hasConnectAccount, onboardingComplete, accountDetails } Creates an onboarding link for Stripe Connect. Returns: { url: string } Generates a login link to the Stripe Express Dashboard. Returns: { url: string, isOnboarding: boolean }

connect.getPayoutHistory

Input: { page: number, limit: number } Returns payout history for the user. Returns: { data: Payout[], pagination: {...} }

connect.getAccountBalance

Returns the user’s Stripe Connect account balance. Returns: { available: number, pending: number, total: number }

connect.getActivity

Input: { page: number, limit: number } Returns combined activity (bounties created and payouts received). Returns: { data: Activity[], pagination: {...} }

Security Best Practices

  • Never expose your Stripe secret key (sk_live_ or sk_test_)
  • Always verify webhook signatures
  • Use HTTPS in production for all Stripe communication
  • Store API keys in environment variables, never in code
  • Regularly rotate webhook secrets
  • Enable Stripe Radar for fraud detection

Production Checklist

  • Switch from test keys to live keys
  • Update webhook endpoint to production URL (HTTPS)
  • Verify Connect is enabled and activated
  • Test payment flow end-to-end
  • Test refund process
  • Test payout to solver Connect account
  • Set up Stripe Radar rules for fraud prevention
  • Configure email receipts in Stripe Dashboard
  • Review and accept Stripe’s terms of service

Build docs developers (and LLMs) love