> For the complete documentation index, see [llms.txt](https://developers.tix.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.tix.xyz/integration-guides/secondary-resale.md).

# Secondary resale integration

This guide takes a marketplace backend from a listing in the catalog to a ticket in the buyer's hands: how money moves, validating a listing, buying it into a claim wallet, tracking the outcome, and delivery. Discovery is covered in [Get started](/getting-started/readme.md).

## How money moves

* Prices are in USDC. `ask_price_minor` is an integer in minor units; USDC has six decimals, so `1000000` is 1 USDC. Read `usdc_decimals` from the protocol config rather than hard-coding it.
* Purchases are paid from your integrator treasury. TDP debits the ask price plus the protocol resale fee, which is `resale_fee_bps` basis points of the price.
* The seller is paid to the payout address fixed on the listing when the purchase confirms.
* Collecting payment from your buyer and paying out your sellers is your marketplace's business. TDP settles the ticket side only.

TDP funds your treasury during onboarding, in sandbox and in production. Ask your TDP representative when it runs low.

Read your treasury balance from the integrator account, using the literal subject `PRIMARY_ADDRESS` for your integrator wallet:

```typescript
const { address } = await client.wallets.getIntegratorAddress({ subject: "PRIMARY_ADDRESS" });
const { integrator } = await client.solana.getIntegrator({ identity: address });
console.log(integrator.balance.amount_minor, integrator.status);
```

## Events, tickets, and listings

* An **event** identifies the performance or occasion. An upcoming event does not guarantee active listings or resale eligibility.
* A **ticket** belongs to an event. A ticket is identified by `event_id` and `ticket_id` together; a ticket ID alone is not globally unique.
* A **listing** is a resale offer for a ticket: asking price, expiry, seller, and an optional buyer restriction. Indexed fields can be null.

A listing is discovery data, not a reservation. Ownership, expiry, restrictions, and resale eligibility are checked when the purchase is accepted, and inventory can change between display and checkout.

## Validate the listing

Find listings with `listActiveListings`, filtered by `eventId` as in [Get started](/getting-started/readme.md#6-find-active-listings). The feed is indexed data, so before quoting a price, read the live listing and the ticket's permit:

```typescript
const ticket = { eventId, ticketId };
const [listing, permit, protocol] = await Promise.all([
  client.solana.getListing(ticket),
  client.solana.getPermit(ticket),
  client.solana.getProtocolConfig(),
]);
```

Don't offer the listing when any of these hold:

* `listing.active` is false, or `listing.expires_at` (Unix seconds) has passed.
* `listing.ask_price_minor` differs from the price you showed the buyer.
* `listing.buyer_allow` is anything other than `11111111111111111111111111111111`, the all-zero address that means any buyer. Any other value reserves the listing for that buyer.
* The permit no longer matches the listing: `permit.version` differs from `listing.permit_version`, `permit.owner` differs from `listing.seller`, or `permit.status` is not `0`. The ticket changed hands or state since it was listed.

Quote the buyer the price plus the fee:

```typescript
const price = BigInt(listing.ask_price_minor);
const fee = (price * BigInt(protocol.resale_fee_bps)) / 10_000n;
const total = price + fee; // minor units; format with protocol.usdc_decimals
```

## Buy into a claim wallet

This flow needs **Accept offers** permission and a registered integrator with a funded treasury. Persist an idempotency key with your order before sending, then purchase:

```typescript
const result = await client.claimWallets.acceptOffers({
  idempotencyKey: `order:${orderId}`,
  body: {
    userSubject: customerId, // the string that identifies this customer in your system
    userEmail: buyerEmail,   // where TDP sends its copy of the claim link
    offers: [{ eventId, ticketId }],
  },
});
```

* Each offer is an event and ticket ID from a listing you validated. A purchase can carry up to 10. TDP fills in the seller payout, royalties, and permit details.
* The idempotency key is a header of 1 to 255 characters. The same key with the same body always refers to the same purchase, so a retry never buys twice. Never mint a new key for an order whose outcome is unknown.

The response carries `status`, `claimWallet` with its `address` and `claimWalletId`, `claimUrl`, and `failureReason` when there is one.

## Track the outcome

| `status`            | Meaning                                                     | Terminal |
| ------------------- | ----------------------------------------------------------- | -------- |
| `processing`        | Submitted, not yet confirmed.                               | No       |
| `confirmed`         | The tickets are in the claim wallet.                        | Yes      |
| `landed_with_error` | The transaction landed but failed. Nothing was bought.      | Yes      |
| `expired`           | The transaction expired before landing. Nothing was bought. | Yes      |
| `null`              | No submission has been recorded yet.                        | No       |

Only `confirmed` means the buyer has tickets. A `claimUrl` is returned while the purchase is still processing, so a claim link alone is not proof of purchase, and HTTP 200 alone is not confirmation.

Check a purchase with the same idempotency key:

```typescript
const status = await client.claimWallets.getPurchaseStatus({ idempotencyKey: `order:${orderId}` });
```

Retrying `acceptOffers` with the same key and body returns the same outcome. After a timeout or an uncertain result, check status before starting a replacement order.

## Deliver the tickets

Sending the claim link to the buyer is your job: email it, or show `claimUrl` on the order page. TDP also emails it to `userEmail` on a best-effort basis, but that copy is not guaranteed to arrive. The recipient opens the link, signs in with KYD Labs, and the tickets move from the claim wallet to their KYD wallet. A sandbox claim needs a KYD test fan; a production claim needs a real one. See [Set up KYD Labs](/getting-started/kyd-setup.md).

Treat `claimUrl` as a credential: keep it out of logs, analytics, and public pages.

## Prepare for production

Review these items with your TDP representative before enabling live resale:

* Production credentials, least-privilege permissions, integrator registration, and treasury funding.
* How your marketplace collects buyer payment and pays sellers, and how refunds and disputes are handled.
* Recovery from timeouts and uncertain outcomes without duplicate purchases.
* Recipient delivery, private claim-link handling, and the support escalation path.

To sell, see [List a ticket for resale](/integration-guides/list-a-ticket.md).
