---
name: sp3nd
description: Purchase shipped products and tokenized Collector Crypt cards through SP3ND with USDC on Solana. Approved Partners can search, quote, create, and pay card orders without a separate opt-in; SP3ND manually purchases and delivers paid cards.
metadata:
  version: 1.11.0
  openclaw:
    requires:
      env:
        - SP3ND_API_KEY
        - SP3ND_API_SECRET
    primaryEnv: SP3ND_API_KEY
    config:
      requiredEnv:
        - SP3ND_API_KEY
        - SP3ND_API_SECRET
      optionalEnv:
        - SOLANA_RPC_URL
        - AGENT_WALLET_PATH
      stateDirs:
        - .wallet.json
---

# SP3ND Agent Purchasing

SP3ND is a purchasing agent that accepts stablecoins and completes supported purchases in fiat. This skill describes the production Agent API contract, including asynchronous pricing and manual review.

> **Payment scope:** this skill documents the optional legacy agentic-skill
> x402 contract for explicitly onboarded non-card orders. A normal server-side Partner
> API integration uses `createPartnerTransaction` for shipped products.
> A normal direct Partner API Collector Crypt integration uses the expiry-bound
> `partnerPayment` prepare/submit contract. Both `createPartnerTransaction` and
> `payAgentOrder` reject real cards. The SP3ND MCP exposes wallet tools at
> `https://mcp.sp3nd.shop/mcp` backed by the same `partnerPayment` endpoint.
> Do not combine payment surfaces.

## Base URL and authentication

```text
https://us-central1-sp3nddotshop-prod.cloudfunctions.net
```

Authenticated requests use:

```http
X-API-Key: <api_key>
X-API-Secret: <api_secret>
```

Keep both credentials and the agent wallet private. Never log or commit secrets or wallet key material.

## Non-negotiable safety rules

1. **SP3ND is authoritative for product data and money.** Submit product URLs and quantities. Never treat caller-supplied title, price, shipping, tax, fee, total, currency, recipient, or memo as authoritative.
2. **An order is not always immediately payable.** Inspect `payment_ready` and `pricing_status` after creation and every refresh.
3. **Only pay when `payment_ready === true`.** `pricing_status` explains the pricing workflow but is never a second payment gate.
4. **Never pay an expired quote.** Check `quote_expires_at` immediately before payment and refresh the order if it has expired or is about to expire.
5. **Use stable idempotency keys.** Reuse the same order key after a timeout or uncertain response. Never generate a new key for a retry of the same intended purchase.
6. **Use the server's payment authorization for the chosen surface.** Collector Crypt amount, currency, treasury, mint, memo, and transaction bytes come from `partnerPayment` prepare. Legacy non-card x402 requirements come from the HTTP 402 response. Do not calculate, rebuild, or override them.
7. **Always attribute end-user orders.** Send the end user's Solana address as `user_wallet`. Without it, an order remains partner-attributed and end-user points or history may not be credited.
8. **Treat order status and pricing status separately.** Fulfillment `status` does not replace `pricing_status`.
9. **Freeze one card buyer wallet.** For Collector Crypt, `user_wallet` and `asset_recipient_wallet` must resolve to the same valid, on-curve wallet. Send both identically. Gifting to a different wallet is not supported in this release.
10. **Do not equate card payment with delivery.** For Collector Crypt, `Paid` confirms SP3ND treasury receipt only. Continue polling until the order confirms wallet delivery; a sold or changed listing may require manual handling and a refund.
11. **Distinguish payment from delivery.** Card orders use `fulfillment_mode: "manual"` and `requires_manual_fulfillment: true`. `Paid` confirms treasury receipt and queues SP3ND's manual acquisition/delivery work; it does not prove the card was delivered.
12. **Use normal approved-Partner authentication.** Collector Crypt search, quotes, orders, and payments need no separate opt-in. `getAgentStatus.agent.collector_crypt_enabled` is a derived discovery capability, always true after approved-Partner authentication, not a stored setting.

## Lifecycle at a glance

```text
register
  -> create server-priced cart
  -> create idempotent order
      -> Ready for Payment
          (`payment_ready: true`; normally `ready_for_payment`)
          -> pay using the order-type-specific server authorization
      -> Awaiting Review
          (`payment_ready: false`; `awaiting_team_quote`)
          -> poll the order
          -> Shipping Selection Required
              (`payment_ready: false`; `shipping_selection_required`)
              -> select a current opaque shipping option
          -> Ready for Payment
          -> pay only after `payment_ready` becomes true
  -> track Paid / Ordered / Shipped / Delivered
```

Collector Crypt uses a separate one-card branch:

```text
search live USDC cards (optional)
  -> five-minute server-priced cart (one card, quantity 1)
  -> order with equal user_wallet + asset_recipient_wallet and no shipping_address
  -> partnerPayment prepare -> sign exact bytes -> submit exact bytes
  -> pay SP3ND in USDC (never createPartnerTransaction)
  -> Paid (treasury receipt only)
  -> pending_acquisition (queued for SP3ND's manual operations team)
  -> SP3ND manually purchases the exact card and transfers it to the buyer wallet
  -> Delivered, or manual/refund handling if the listing became unavailable
```

Mixed carts and items SP3ND cannot verify automatically remain one order. They are not split by the caller and must not be paid early.
An order in `Awaiting Review` must first reach the conceptual **Ready for Payment** state before it can be paid. This state means `payment_ready === true`, the quote is current, and any required shipping option is selected. Do not transition directly from `Awaiting Review` to `Paid`.

The public `checkout_role` vocabulary is exactly `direct_payment` and `manual_review`. Older stored role names are normalized before an order is returned.
The primary public `pricing_status` vocabulary is exactly `awaiting_team_quote`, `shipping_selection_required`, and `ready_for_payment`. Treat it as workflow information; `payment_ready` remains the sole payment permission.

## 1. Register an agent

```http
POST /registerAgent
Content-Type: application/json

{
  "agent_name": "MyShoppingBot",
  "solana_public_key": "<agent-wallet-public-key>",
  "contact_email": "operator@example.com",
  "description": "Purchases products for approved users"
}
```

The response returns `api_key` and a one-time `api_secret`. Save the secret immediately.

If the secret is lost, call `POST /regenerateAgentSecret` with the saved API key
and a signature from the registered wallet over exactly
`SP3ND:partner-api:regenerate-secret:v1:<api-key>:<registered-wallet>:<13-digit-ms-timestamp>:<64-hex-char-random-nonce>`.
The timestamp must be fresh and each signed message is single-use. Regeneration
invalidates the old secret; duplicate registration never returns an existing
API key.

## 2. Search Collector Crypt cards (optional)

```http
GET /searchCollectorCryptCards?q=charizard%20psa%2010&limit=20
X-API-Key: <api_key>
X-API-Secret: <api_secret>
```

This surface is available with normal authenticated, approved Partner credentials;
no separate Collector Crypt opt-in is required. `q` is optional and limited to
200 characters. Upstream it performs a substring match against card/item name
fields and an exact match against NFT address or grading ID. Omit it to browse.
`limit` defaults to `20` and
accepts `1..100`. Use either a
1-based `page` from `1..10000` or the unchanged opaque `next_cursor` as
`cursor`; cursor takes precedence. A cursor must be nonblank, at most 2048
characters, and contain no control characters.

The response is `{ success, schema_version: 2, provider: "collectorcrypt", currency: "USDC", items, next_cursor, total, marketplace_total, total_pages, category_counts }`. Each item includes generic `price`/`currency` plus `pricing_source`, `card_id`, `back_image_url`, `card_type`, `grading_id`, `listing_price_usdc`, `insured_value_usd`, `listed_at`, and `listing_updated_at`. Returned items use a supported `token_standard` (`Pnft`, `Cnft`, `StandardNft`, or `CoreNft`) and an exact cent-denominated price. The upstream count fields are computed before SP3ND's local eligibility filters and may exceed `items.length`; they and insured value are informational, not payment authority. Search results are discovery data, not a reservation or payment authority. `createPartnerCart` refetches the selected asset.

## 3. Create a server-priced cart

Use only product URLs and quantities as purchasing inputs:

```http
POST /createPartnerCart
Content-Type: application/json
X-API-Key: <api_key>
X-API-Secret: <api_secret>

{
  "items": [
    {
      "product_url": "https://www.amazon.com/dp/B08XYZ123",
      "quantity": 1
    },
    {
      "product_url": "https://www.ebay.com/itm/123456789",
      "quantity": 1
    }
  ]
}
```

Do not send a caller-selected `price` to control the purchase. Any display metadata accepted for compatibility is non-authoritative; SP3ND resolves the listing and computes purchasable values.

Cart totals can still be provisional. In particular, eBay availability and shipping depend on the destination. Always provide the final destination when creating the order.

For Collector Crypt, create a separate cart with exactly one card and quantity `1`. Do not mix it with shipped products. Omit destination fields and use either the exact canonical URL `https://collectorcrypt.com/assets/solana/<asset-address>` or:

```json
{
  "user_wallet": "<buyer-solana-wallet>",
  "items": [{
    "provider": "collectorcrypt",
    "nft_address": "<asset-address>",
    "quantity": 1
  }]
}
```

SP3ND ignores caller-supplied price, seller, token standard, and listing metadata. A Collector Crypt cart expires after 5 minutes. The optional top-level `user_wallet` must be a valid on-curve Solana address; SP3ND stores it on the cart and uses it if the order request omits `user_wallet`. Invalid cart wallets return `USER_WALLET_INVALID`. Final buyer/recipient equality is enforced at order creation.

## 4. Create an idempotent order

Create one stable key for the user's intended checkout. Persist it before sending the request.

```http
POST /createPartnerOrder
Content-Type: application/json
X-API-Key: <api_key>
X-API-Secret: <api_secret>
Idempotency-Key: checkout_01JSTABLEKEY

{
  "cart_id": "cart_abc123",
  "idempotency_key": "checkout_01JSTABLEKEY",
  "user_wallet": "<end-user-solana-wallet>",
  "customer_email": "customer@example.com",
  "shipping_address": {
    "name": "John Doe",
    "recipient": "John Doe",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "San Francisco",
    "state": "CA",
    "postalCode": "94105",
    "country": "United States",
    "countryCode": "US",
    "phone": "+14155551234"
  }
}
```

Send the stable key in either the `Idempotency-Key` header or the `idempotency_key` body field. Supplying both with the same value is recommended for broad client compatibility. If both are omitted by a legacy integration, the server falls back to `cart_id`, but new integrations must supply an explicit key.

For eBay, the complete destination is required for the location-aware offer and shipping check. Do not assume a price from a different country, postal code, or eBay TLD remains valid.

For a Collector Crypt-only cart, omit `shipping_address` and send:

```json
{
  "cart_id": "cart_card123",
  "idempotency_key": "card_checkout_01JSTABLEKEY",
  "user_wallet": "<buyer-solana-wallet>",
  "asset_recipient_wallet": "<buyer-solana-wallet>",
  "customer_email": "customer@example.com"
}
```

The two wallet fields must resolve to the same valid, on-curve Solana address and are frozen into the idempotent order. If one is omitted the server copies the other, but new integrations should send both identically. `ASSET_RECIPIENT_WALLET_MISMATCH` means stop; gifting is unsupported.

### Schema v2 lifecycle fields

Create and order-read responses include additive `schema_version: 2` lifecycle fields. Existing fields remain available.

A verified-only order can be created in a payable state:

```json
{
  "success": true,
  "schema_version": 2,
  "order": {
    "order_id": "firebase-document-id",
    "order_number": "ORD-1234567890",
    "status": "Created",
    "checkout_role": "direct_payment",
    "pricing_status": "ready_for_payment",
    "requires_manual_quote": false,
    "payment_ready": true,
    "quote_revision": 1,
    "quote_expires_at": "2026-07-26T18:30:00.000Z",
    "shipping_options": [],
    "selected_shipping_option_id": null,
    "selected_shipping_option": null
  }
}
```

A mixed or unverified order remains intact and enters review:

```json
{
  "success": true,
  "schema_version": 2,
  "order": {
    "order_id": "firebase-document-id",
    "order_number": "ORD-1234567890",
    "status": "Awaiting Review",
    "checkout_role": "manual_review",
    "pricing_status": "awaiting_team_quote",
    "requires_manual_quote": true,
    "payment_ready": false,
    "quote_revision": 0,
    "quote_expires_at": null,
    "shipping_options": [],
    "selected_shipping_option_id": null,
    "selected_shipping_option": null
  }
}
```

For an order in review:

- Show the user that SP3ND is reviewing pricing or shipping.
- Do not call a payment endpoint.
- Poll the singular order endpoint at a reasonable interval or refresh on user action.
- Continue only when the server returns a current payable quote.

## 5. Read orders

Read one order when waiting for a quote or tracking a checkout:

```http
GET /getPartnerOrder?order_id=<order_id>
X-API-Key: <api_key>
X-API-Secret: <api_secret>
```

List the authenticated partner's orders:

```http
GET /getPartnerOrders
X-API-Key: <api_key>
X-API-Secret: <api_secret>
```

**To list all orders, omit `status` (recommended).** `status=all` is also supported for compatibility. Otherwise, use `status` only for a real named order status such as `Created`, `Awaiting Review`, `Paid`, `Ordered`, `Shipped`, or `Delivered`.

Order-list pagination uses integer `limit` (`1..100`, default `50`) and `offset` (`0..499`, default `0`), with `limit + offset <= 500`. Optional `user_wallet` and `customer_email` filters are applied before pagination.

Order reads expose the same additive schema v2 lifecycle fields as order creation. Preserve unknown fields for forward compatibility.

## 6. Select a quoted shipping option

When `pricing_status === "shipping_selection_required"`, present the server-returned `shipping_options` to the user. Every option uses `shipping_option_id` as its opaque identifier. Do not reconstruct an option's price or delivery estimate.

The canonical option shape is:

- `shipping_option_id`
- `label`
- optional `description`
- optional `estimated_delivery`
- `shipping_amount`
- `tax_amount`
- `total_amount`
- `currency`

The additive aliases `id`, `name`, `min_days`, and `max_days` may appear for compatibility. Always POST the canonical opaque `shipping_option_id`.

`selected_shipping_option_id` is the canonical selected identifier. `selected_shipping_option` contains the selected option in the normalized shape above. `selected_manual_shipping_option` is a deprecated compatibility alias for that same normalized object.

```http
POST /selectPartnerOrderShippingOption
Content-Type: application/json
X-API-Key: <api_key>
X-API-Secret: <api_secret>

{
  "order_id": "<order_id>",
  "shipping_option_id": "<opaque-option-id>"
}
```

The server validates the quote revision and expiry. Use the returned order as the new source of truth. If the selection is rejected because the quote changed or expired, refresh the order and ask the user to choose from the new options.

Before payment, confirm:

- `selected_shipping_option_id` and `selected_shipping_option` match the user's current choice when a choice is required.
- `quote_revision` is the latest revision returned by the server.
- `quote_expires_at` has not passed.
- `payment_ready === true`.

`pricing_status` remains useful for rendering the workflow, but it must not be used as an additional payment gate.

## 7. Pay a payment-ready order on its required surface

### Collector Crypt: `partnerPayment` prepare/submit

Approved Partners can create and pay real card orders without a separate
Collector Crypt opt-in. Orders use `fulfillment_mode: "manual"` and
`requires_manual_fulfillment: true`: confirmed payment queues SP3ND's operations
team to purchase the exact card and transfer it to the fixed buyer wallet.
There is no automated purchase or transfer signer in this flow. Fulfillment
records use confirmed transaction references and authorized operator attestations;
delivery also checks current asset ownership. These checks do not prove that a
referenced transaction caused the specific purchase or transfer.

Prepare exactly one immutable, expiry-bound payment:

```http
POST /partnerPayment
Content-Type: application/json
X-API-Key: <api_key>
X-API-Secret: <api_secret>

{
  "action": "prepare",
  "order_id": "<order_id>",
  "payer_address": "<buyer-solana-wallet>"
}
```

A new prepare returns HTTP `201` with `status: "payment_prepared"`. The response
includes `idempotent_replay`, order identifiers, `amount`, `currency`,
`network`, `payer_address`, `recipient_address`, `token_mint`,
`token_decimals`, string `amount_atomic`, `memo`,
`unsigned_transaction_base64`, `recent_blockhash`, and
`last_valid_block_height`. HTTP `200` with `idempotent_replay: true` safely
returns the same still-valid bytes for an unchanged order and payer.

Verify those fields and have exactly `payer_address` sign the exact returned
transaction. Never rebuild it, change an instruction, replace the blockhash,
or sign another authorization. On `PAYMENT_TRANSACTION_NEAR_EXPIRY`, do not
sign; wait until the returned last valid blockheight passes before preparing
again.

Submit only the exact signed serialization:

```http
POST /partnerPayment
Content-Type: application/json
X-API-Key: <api_key>
X-API-Secret: <api_secret>

{
  "action": "submit",
  "order_id": "<order_id>",
  "signed_transaction_base64": "<exact-wallet-signed-transaction>"
}
```

A new submit normally returns HTTP `202` with
`status: "verifying_payment"`, the order identifiers, and
`transaction_signature`. An idempotent replay or `already_paid` result may use
HTTP `200`. `verifying_payment` and a transaction signature do not prove
treasury receipt; poll `getPartnerOrder` until `Paid`. If the response is lost
or `PAYMENT_BROADCAST_UNKNOWN` is returned, read the order and preserve the
original signed bytes. Retry only the exact same
`signed_transaction_base64`; never rebuild, re-sign, or prepare a replacement
while the outcome is unknown.

`createPartnerTransaction` is not a fallback. It rejects a real card order with
HTTP `409` and `COLLECTOR_CRYPT_PAYMENT_METHOD_UNSUPPORTED`.

### Explicitly onboarded agentic-skill x402 contract

Start payment using the canonical order ID:

```http
POST /payAgentOrder
Content-Type: application/json
X-API-Key: <api_key>
X-API-Secret: <api_secret>

{
  "order_id": "<order_id>"
}
```

`order_number` is optional. If supplied, the server validates that it belongs to `order_id`.

Call `payAgentOrder` directly for this explicitly supported legacy flow. Do not
call `createPartnerTransaction` first; that is the alternative
standard Partner API shipped-product payment path. Real Collector Crypt orders
are rejected here with `COLLECTOR_CRYPT_PAYMENT_METHOD_UNSUPPORTED`; use
`partnerPayment` only.

The initial call returns HTTP 402 with payment requirements in the `PAYMENT-REQUIRED` header and response body. Those requirements are authoritative:

```json
{
  "x402Version": 2,
  "accepts": [
    {
      "scheme": "exact",
      "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
      "maxAmountRequired": "<server-value>",
      "resource": "<server-value>",
      "payTo": "<server-value>",
      "asset": "<server-value>",
      "extra": {
        "order_id": "<order_id>",
        "order_number": "<order-number>",
        "feePayer": "<server-value>"
      }
    }
  ]
}
```

Build and sign the transfer strictly from the returned requirements. Include the exact memo specified by the response. Never reuse a 402 response from another order or quote revision.

Submit the signed result back to the same SP3ND endpoint in the `PAYMENT-SIGNATURE` header:

```http
POST /payAgentOrder
Content-Type: application/json
X-API-Key: <api_key>
X-API-Secret: <api_secret>
PAYMENT-SIGNATURE: <signed-payment-payload>

{
  "order_id": "<order_id>"
}
```

Do not call, configure, or authenticate with a facilitator directly. SP3ND's `payAgentOrder` endpoint is the sole settlement authority. After submitting the signature to SP3ND, poll `GET /getPartnerOrder?order_id=...` until the order is `Paid`.

- `PAYMENT_SETTLEMENT_IN_PROGRESS`: do not submit another payment; keep reading the order until settlement resolves.
- `PAYMENT_SETTLEMENT_UNKNOWN` with `retryable: false`: do not retry payment. Retain the `order_id` and escalate for manual reconciliation because the transfer may have reached the network.

### Alternative standard shipped-product Partner API payment

A server-side Partner API integration that does not intentionally implement
the agentic-skill x402 contract should use `POST /createPartnerTransaction`
instead for shipped products. Collector Crypt must use `partnerPayment` and
receives `COLLECTOR_CRYPT_PAYMENT_METHOD_UNSUPPORTED` here. Identify the
shipped purchase with:

```json
{
  "order_id": "<order_id>"
}
```

`order_number` may also be supplied and is validated against `order_id`. Caller-supplied `amount`, `currency`, `memo`, and recipient fields are ignored. Use only the server-returned payment instructions. Build and broadcast the wallet payment, then poll the order while SP3ND confirms it asynchronously. Do not call `payAgentOrder` for the same payment.

### MCP is separate

MCP clients connect to `https://mcp.sp3nd.shop/mcp` and use its advertised
OAuth-protected tools. Those tools use the same `partnerPayment` backend as the
card Partner API flow, while binding it to the MCP OAuth session and approval
ticket. MCP clients never receive Partner API credentials.

## 8. Track fulfillment

Once paid, monitor the order's fulfillment `status` independently of `pricing_status`.

Pricing and payment gate:

```text
Awaiting Review -> Ready for Payment -> Paid
```

`Ready for Payment` means `payment_ready === true`; it is not inferred from `pricing_status`. Verified-only orders may begin in that state without entering review.

Fulfillment after payment:

```text
Paid -> Ordered -> Shipped -> Delivered
```

Do not infer shipment or delivery from payment alone.

For Collector Crypt, the buyer pays SP3ND in USDC first. `Paid` confirms only the treasury receipt. Payment confirmation creates a server-only fulfillment job and normally moves `fulfillment_status` from `awaiting_payment` to `pending_acquisition`; a payment/listing claim mismatch uses `review_required`. Pending means queued for SP3ND's manual operations team to purchase the exact card and deliver it to the fixed buyer wallet; no automatic purchase or transfer has occurred. `provider_purchase_signature` records a confirmed transaction associated with the purchase by an authorized operator; it does not cryptographically prove purchase causality or final wallet delivery. Authorized operations must atomically record delivered fulfillment metadata and transfer evidence with top-level `status: "Delivered"` only after verifying the transfer; clients use that top-level status as the completion gate and may display `asset_transfer_signature`. The listing may sell or change before SP3ND buys it, in which case the order may require manual handling and a refund. Never select a replacement card or change the buyer wallet client-side.

## Retry and idempotency behavior

- Persist `cart_id`, `idempotency_key`, `order_id`, and `order_number`.
- Retry order creation with the original `Idempotency-Key`.
- After a timeout, call `GET /getPartnerOrder` or list orders before retrying.
- Never create a second cart/order solely because the response was lost.
- Never pay twice because a settlement response was lost. Read the order first, and stop for manual reconciliation if the API reports `PAYMENT_SETTLEMENT_UNKNOWN`.
- For a `partnerPayment` submit retry, preserve and resend only the exact same `signed_transaction_base64`. Never re-sign or prepare replacement bytes while the first result is unknown.
- Do not cache a quote beyond `quote_expires_at`.

## Marketplace and destination rules

Use a marketplace URL suitable for the recipient's destination, then let SP3ND verify the actual listing.

### Amazon storefronts

| Region | Storefront |
|---|---|
| United States | `amazon.com` |
| United Kingdom | `amazon.co.uk` |
| Canada | `amazon.ca` |
| Germany | `amazon.de` |
| France | `amazon.fr` |
| Spain | `amazon.es` |
| Italy | `amazon.it` |
| Netherlands | `amazon.nl` |
| Belgium | `amazon.com.be` |
| Belgium (legacy) | `amazon.be` |
| Ireland | `amazon.ie` |
| Poland | `amazon.pl` |
| Sweden | `amazon.se` |
| Brazil | `amazon.com.br` |
| Mexico | `amazon.com.mx` |
| Australia | `amazon.com.au` |
| India | `amazon.in` |
| Japan | `amazon.co.jp` |
| Singapore | `amazon.sg` |
| United Arab Emirates | `amazon.ae` |
| Saudi Arabia | `amazon.sa` |
| Egypt | `amazon.eg` |
| Turkey | `amazon.com.tr` |
| South Africa | `amazon.co.za` |
| China | `amazon.cn` |

Amazon product-share links from `a.co`, `amzn.to`, `amzn.eu`, and
`amzn.asia` are accepted and resolved to a canonical product URL.

### eBay storefronts

| Region | Storefront |
|---|---|
| United States | `ebay.com` |
| Canada | `ebay.ca` |
| United Kingdom | `ebay.co.uk` |
| Austria | `ebay.at` |
| Belgium | `ebay.be` |
| Switzerland | `ebay.ch` |
| Germany | `ebay.de` |
| France | `ebay.fr` |
| Italy | `ebay.it` |
| Spain | `ebay.es` |
| Australia | `ebay.com.au` |
| Hong Kong | `ebay.com.hk` |
| Ireland | `ebay.ie` |
| Malaysia | `ebay.com.my` |
| Netherlands | `ebay.nl` |
| Philippines | `ebay.ph` |
| Poland | `ebay.pl` |
| Singapore | `ebay.com.sg` |
| Taiwan | `ebay.com.tw` |
| India | `ebay.in` |
| China | `ebay.cn` |
| Vietnam | `ebay.vn` |
| Thailand | `ebay.co.th` |

eBay product shares in the form `ebay.io/m/<code>` are accepted. SP3ND
validates the redirect and stores the canonical eBay item URL.

eBay seller availability, item price, and shipping can change with destination. The final order address is always authoritative.

### Collector Crypt cards

Use `GET /searchCollectorCryptCards` for live discovery. Eligible card URLs have the exact form `https://collectorcrypt.com/assets/solana/<asset-address>`. Only native Solana Collector Crypt V2 buy-now listings priced in USDC with a supported token standard and cent-denominated price are returned. A cart contains one card with quantity `1` and uses no physical shipping address. Approved Partners can search, quote, create, and pay without a separate opt-in. Card orders use manual fulfillment, and the card payment uses `partnerPayment` only, and the tokenized claim goes to the buyer wallet shared by `user_wallet` and `asset_recipient_wallet` after SP3ND completes acquisition and transfer.

## Minimal lifecycle example

```javascript
const BASE_URL = 'https://us-central1-sp3nddotshop-prod.cloudfunctions.net';
const auth = {
  'Content-Type': 'application/json',
  'X-API-Key': process.env.SP3ND_API_KEY,
  'X-API-Secret': process.env.SP3ND_API_SECRET,
};

async function json(response) {
  const body = await response.json();
  if (!response.ok) throw new Error(body.error || `${response.status}`);
  return body;
}

async function getOrder(orderId) {
  const result = await json(await fetch(
    `${BASE_URL}/getPartnerOrder?order_id=${encodeURIComponent(orderId)}`,
    { headers: auth },
  ));
  return result.order ?? result;
}

async function beginCheckout({ productUrl, userWallet, email, shippingAddress, checkoutKey }) {
  const cartResult = await json(await fetch(`${BASE_URL}/createPartnerCart`, {
    method: 'POST',
    headers: auth,
    body: JSON.stringify({
      user_wallet: userWallet,
      items: [{ product_url: productUrl, quantity: 1 }],
    }),
  }));

  const orderResult = await json(await fetch(`${BASE_URL}/createPartnerOrder`, {
    method: 'POST',
    headers: { ...auth, 'Idempotency-Key': checkoutKey },
    body: JSON.stringify({
      cart_id: cartResult.cart.cart_id,
      idempotency_key: checkoutKey,
      user_wallet: userWallet,
      customer_email: email,
      shipping_address: shippingAddress,
    }),
  }));

  return orderResult.order;
}

function assertPaymentReady(order) {
  const unexpired = !order.quote_expires_at ||
    Date.parse(order.quote_expires_at) > Date.now();

  if (!order.payment_ready || !unexpired) {
    throw new Error('Order is not ready for payment; refresh it instead.');
  }

  if (
    order.pricing_status === 'shipping_selection_required' ||
    (order.shipping_options?.length && !order.selected_shipping_option_id)
  ) {
    throw new Error('Select a current shipping option before payment.');
  }
}

async function requestPayment(order) {
  assertPaymentReady(order);
  if (order.order_type === 'collector_crypt_card') {
    return fetch(`${BASE_URL}/partnerPayment`, {
      method: 'POST',
      headers: auth,
      body: JSON.stringify({
        action: 'prepare',
        order_id: order.order_id,
        payer_address: order.user_wallet,
      }),
    });
  }
  return fetch(`${BASE_URL}/payAgentOrder`, {
    method: 'POST',
    headers: auth,
    body: JSON.stringify({ order_id: order.order_id }),
  });
}

async function submitExactCardPayment(orderId, signedTransactionBase64) {
  return fetch(`${BASE_URL}/partnerPayment`, {
    method: 'POST',
    headers: auth,
    body: JSON.stringify({
      action: 'submit',
      order_id: orderId,
      signed_transaction_base64: signedTransactionBase64,
    }),
  });
}
```

## Operator controls

- Use a dedicated, least-funded wallet for the agent.
- Add an application-level approval limit before order creation or payment.
- Validate the recipient and destination before creating the order.
- Show review, quote expiry, shipping choice, and total changes to the user.
- Never expose a private shipping address to an unrelated purchaser.

## Support

- API documentation: https://sp3nd.shop/partner-api/docs
- Partner dashboard: https://sp3nd.shop/partner-api/dashboard
- Support: support@sp3nd.shop
