# API Reference
Source: https://spiceflow-docs.spicenet.io/api-reference
Complete API documentation for cross-chain transaction submission and intent execution
## Authentication
The API uses signature-based authentication. For EIP-7702 mode, you sign authorizations with your wallet. For non-7702 modes, you sign or submit transactions directly.
## Response Format
All API responses follow this format:
```json theme={null}
{
"success": boolean,
"data": object | null,
"error": { "message": string } | null
}
```
## Error Handling
| Status | Description |
| --------- | ---------------------------------- |
| `200/201` | Success |
| `202` | Accepted (step already processing) |
| `400` | Bad Request (validation error) |
| `404` | Not Found |
| `500` | Internal Server Error |
***
## Endpoints Overview
### Actions API
The unified API for creating and executing cross-chain intents.
| Method | Endpoint | Description |
| ---------- | -------------------------------------------- | -------------------------------------------------------------------- |
| `POST` | `/actions` | [Create action with intents](/api-reference/endpoint/actions-create) |
| `GET` | `/actions/:actionId` | [Get action status](/api-reference/endpoint/actions-get) |
| `GET` | `/actions/:actionId/intents/:idx` | [Get intent status](/api-reference/endpoint/actions-get) |
| `GET/POST` | `/actions/:actionId/intents/:idx/steps/:idx` | [Get/Execute step](/api-reference/endpoint/actions-execute) |
Create an action with intents for cross-chain execution
Retrieve action, intent, or step status
Execute a specific intent step
***
### Balances & Deposits
Manage user balances for cross-chain transfers.
| Method | Endpoint | Description |
| ------ | --------------------------- | ---------------------------------------------------------------- |
| `GET` | `/wallets/:address/balance` | [Get user Spice balances](/api-reference/endpoint/spice-deposit) |
Record deposits, withdrawals, and query balances
***
### Transaction Tracking
Register an on-chain transaction for the relayer to process.
| Method | Endpoint | Description |
| ------ | -------------------------- | --------------------------------------------------------------- |
| `POST` | `/transactions/handle/evm` | [Handle a transaction](/api-reference/endpoint/venue-tx-hashes) |
Register an on-chain transaction hash for processing
***
### Utility
| Method | Endpoint | Description |
| ------ | ---------------------------- | -------------------------------------------------- |
| `POST` | `/airdrop/:chainId/:tokenId` | [Request airdrop](/api-reference/endpoint/airdrop) |
| `GET` | `/health` | [Health check](/api-reference/endpoint/health) |
Request testnet token airdrops
Check API server status
***
### Legacy Endpoints
These endpoints are deprecated and not served by the current relayer. Use the Actions API above.
| Method | Endpoint | Description |
| ------ | ----------------------------- | ----------------------------------------------------------------------- |
| `POST` | `/transaction/submit` | [Submit legacy transaction](/api-reference/endpoint/transaction-submit) |
| `GET` | `/intent/:id/step/:id/status` | [Get step status](/api-reference/endpoint/intent-step-status) |
Submit a cross-chain transaction (legacy)
Get intent step status (legacy)
# POST /actions
Source: https://spiceflow-docs.spicenet.io/api-reference/endpoint/actions-create
Create a new action with one or more intents for cross-chain execution
## Endpoint
```http theme={null}
POST /actions
```
## Description
This is the **unified entry point** for creating cross-chain intents. An action groups related intents together and provides a single tracking ID. Each intent within an action can have its own execution mode and chain batches.
## Request Body
```typescript theme={null}
{
user: Address; // User wallet address
chainAuthorizations?: ChainAuth7702[]; // Required for 7702 mode
intents: Intent[]; // Array of intents (min 1)
}
```
### Execution Modes
`non-7702:presign` mode is **not yet implemented**. Only `7702` and `non-7702:on-demand` modes are currently functional.
EIP-7702 mode uses delegate contract for execution.
```typescript theme={null}
{
mode: "7702",
signatureType: string,
signature: Hex,
nbf: number, // Not before timestamp
exp: number, // Expiration timestamp
chainBatches: [{
hash: Hex,
chainId: number,
tokenTransfers: TokenTransfer[],
calls: Call[]
}]
}
```
This mode exists in schema but execution is not yet implemented.
Presigned transactions for execution by the solver.
```typescript theme={null}
{
mode: "non-7702:presign",
chainBatches: [{
chainId: number,
tokenTransfers: TokenTransfer[],
call: {
to: Address,
value: bigint,
data: Hex,
signature: Hex // User's presigned tx signature
}
}]
}
```
User sends transactions themselves, registers hash for solver fulfillment.
```typescript theme={null}
{
mode: "non-7702:on-demand",
chainBatches: [{
chainId: number,
tokenTransfers: TokenTransfer[],
call: {
to: Address,
value: bigint,
data: Hex
}
}]
}
```
### Schema Details
#### TokenTransfer
| Field | Type | Description |
| -------- | --------------------- | -------------------------------------------- |
| `from` | `Address \| "solver"` | Sender address or "solver" for solver-funded |
| `to` | `Address` | Recipient address |
| `token` | `Address` | Token contract address |
| `amount` | `bigint` | Amount in wei |
#### ChainAuthorization (7702 only)
| Field | Type | Required | Description |
| --------- | --------- | -------- | ------------------------ |
| `address` | `Address` | Yes | Address being authorized |
| `chainId` | `number` | Yes | Chain ID |
| `nonce` | `number` | Yes | Authorization nonce |
| `r` | `Hex` | Yes | ECDSA signature R value |
| `s` | `Hex` | Yes | ECDSA signature S value |
| `yParity` | `number` | No | Y parity (default: 0) |
## Response
### Success Response (201)
```json theme={null}
{
"actionId": "act_a1b2c3d4e5f6...",
"intentIds": [
"act_a1b2c3d4e5f6.../0",
"act_a1b2c3d4e5f6.../1"
]
}
```
| Field | Type | Description |
| ----------- | ---------- | -------------------------------------------------- |
| `actionId` | `string` | Unique action identifier |
| `intentIds` | `string[]` | Array of intent IDs (format: `{actionId}/{index}`) |
### Error Responses
```json theme={null}
{
"error": "schema validation failed",
"issues": { /* Zod validation errors */ }
}
```
```json theme={null}
{
"error": "chainAuthorizations required for 7702 intents"
}
```
## Example Request
```bash theme={null}
curl -X POST /actions \
-H "Content-Type: application/json" \
-d '{
"user": "0x742d35Cc6634C0532925a3b844e4B7db0D6d8E5c",
"chainAuthorizations": [{
"address": "0x742d35Cc6634C0532925a3b844e4B7db0D6d8E5c",
"chainId": 84532,
"nonce": 1,
"r": "0x...",
"s": "0x...",
"yParity": 0
}],
"intents": [{
"mode": "7702",
"signatureType": "eip712",
"signature": "0x...",
"nbf": 0,
"exp": 1999999999,
"chainBatches": [{
"hash": "0x...",
"chainId": 84532,
"tokenTransfers": [{
"from": "0x742d35Cc6634C0532925a3b844e4B7db0D6d8E5c",
"to": "0xVenueAddress...",
"token": "0xTokenAddress...",
"amount": "1000000000000000000"
}],
"calls": [{
"to": "0xTokenAddress...",
"value": "0",
"data": "0xa9059cbb..."
}]
}]
}]
}'
```
## Workflow
1. **Create Action**: Submit action with intents
2. **Track Progress**: Use returned `actionId` and `intentIds` to monitor status
3. **Execute Steps**: Call `POST /actions/:actionId/intents/:intentIndex/steps/:stepIndex` for each step
## Next Steps
After creating an action:
* [Get Action Status](/api-reference/endpoint/actions-get) - Monitor action and intent status
* [Execute Step](/api-reference/endpoint/actions-execute) - Execute individual steps
# POST Execute Step
Source: https://spiceflow-docs.spicenet.io/api-reference/endpoint/actions-execute
Execute a specific intent step based on execution mode
## Endpoint
```http theme={null}
POST /actions/:actionId/intents/:intentIndex/steps/:stepIndex
```
## Path Parameters
| Parameter | Type | Required | Description |
| ------------- | -------- | -------- | ---------------------- |
| `actionId` | `string` | Yes | Action ID |
| `intentIndex` | `number` | Yes | Intent index (0-based) |
| `stepIndex` | `number` | Yes | Step index (0-based) |
`non-7702:presign` mode is **not yet implemented**. Only `7702` and `non-7702:on-demand` modes are currently functional.
## Request Body by Mode
For EIP-7702 intents, simply trigger execution:
```json theme={null}
{
"action": "execute"
}
```
The solver executes the delegated calls using the stored authorization.
This mode exists in schema but execution is not yet implemented.
For presigned transactions:
**Register hash (store for later):**
```json theme={null}
{
"action": "register",
"hash": "0xTransactionHash..."
}
```
**Execute (broadcast stored tx):**
```json theme={null}
{
"action": "execute"
}
```
User sends their own transaction, then notifies the API:
**Register (store hash only, for logging):**
```json theme={null}
{
"action": "register",
"hash": "0xUserSubmittedTxHash..."
}
```
**Execute (verify tx + update balances + trigger solver):**
```json theme={null}
{
"action": "execute",
"hash": "0xUserSubmittedTxHash..."
}
```
## Response
### Success Response (200)
```json theme={null}
{
"status": "success",
"transactionHash": "0x..."
}
```
| Field | Type | Description |
| ----------------- | -------- | -------------------------------- |
| `status` | `string` | Step status after execution |
| `transactionHash` | `string` | Transaction hash (if applicable) |
### Already Processed (202)
If the step is not in `created` status:
```json theme={null}
{
"status": "executing"
}
```
## Execution Flow
### 7702 Mode
1. Solver retrieves stored calls and authorization
2. Broadcasts transaction with delegated execution
3. Updates step status on confirmation
### non-7702:on-demand Mode
When `action: "execute"` with a transaction hash:
1. **Verify Transaction**: Check tx exists on-chain and succeeded
2. **Replay Prevention**: Reject if hash already processed
3. **Update Balances**: Credit user's Spice balance from token transfers
4. **Execute Solver Transfers**: Send tokens on destination chain(s)
5. **Auto-Advance**: Automatically execute remaining steps (solver transfers)
When `action: "register"`:
1. **Verify Transaction**: Check tx exists and succeeded
2. **Store Reference**: Save hash for tracking, no balance updates
3. **Status**: Set to `registered` instead of `success`
## Error Responses
### 400 Bad Request
```json theme={null}
{
"error": "schema validation failed",
"issues": { /* Validation details */ }
}
```
```json theme={null}
{
"error": "Transaction already processed"
}
```
```json theme={null}
{
"error": "Transaction failed or pending"
}
```
### 404 Not Found
```json theme={null}
{
"error": "could not find intent",
"actionId": "act_...",
"intentIndex": 0,
"stepIndex": 0
}
```
### 500 Internal Server Error
```json theme={null}
{
"error": "error executing intent step",
"actionId": "act_...",
"intentIndex": 0,
"stepIndex": 0,
"message": "Error details..."
}
```
## Example Requests
### 7702 Mode
```bash theme={null}
curl -X POST /actions/act_abc123/intents/0/steps/0 \
-H "Content-Type: application/json" \
-d '{"action": "execute"}'
```
### non-7702:on-demand Mode
```bash theme={null}
# User sends their own transaction first, then:
curl -X POST /actions/act_abc123/intents/0/steps/0 \
-H "Content-Type: application/json" \
-d '{
"action": "execute",
"hash": "0x1234567890abcdef..."
}'
```
## Notes
* Steps must be executed in order (0, 1, 2, ...)
* For `non-7702:on-demand`, the user is responsible for sending the source chain transaction
* After a successful `execute` action, remaining steps (solver transfers on destination chains) are auto-executed
# GET Action & Intent Status
Source: https://spiceflow-docs.spicenet.io/api-reference/endpoint/actions-get
Retrieve action, intent, or step status for tracking execution progress
## Endpoints
| Method | Endpoint | Description |
| ------ | ---------------------------------------------------------- | -------------------------------- |
| `GET` | `/actions/:actionId` | Get full action with all intents |
| `GET` | `/actions/:actionId/intents/:intentIndex` | Get specific intent |
| `GET` | `/actions/:actionId/intents/:intentIndex/steps/:stepIndex` | Get specific step |
***
## GET /actions/:actionId
Retrieve a complete action including all intents and their chain authorization steps.
### Path Parameters
| Parameter | Type | Required | Description |
| ---------- | -------- | -------- | --------------------------------- |
| `actionId` | `string` | Yes | Action ID (e.g., `act_abc123...`) |
### Response (200)
```json theme={null}
{
"id": "act_abc123...",
"user": "0x742d35Cc6634C0532925a3b844e4B7db0D6d8E5c",
"status": "created",
"intents": [
{
"id": "act_abc123.../0",
"mode": "7702",
"status": "created",
"executionIndex": 0,
"chainAuthorizations": [
{
"index": 0,
"chainId": "84532",
"status": "created",
"mode": "7702"
}
]
}
]
}
```
***
## GET /actions/:actionId/intents/:intentIndex
Retrieve a specific intent within an action.
### Path Parameters
| Parameter | Type | Required | Description |
| ------------- | -------- | -------- | ---------------------- |
| `actionId` | `string` | Yes | Action ID |
| `intentIndex` | `number` | Yes | Intent index (0-based) |
### Response (200)
```json theme={null}
{
"id": "act_abc123.../0",
"actionId": "act_abc123...",
"mode": "7702",
"status": "created",
"executionIndex": 0,
"signatureType": "eip712",
"signature": "0x...",
"nbf": "0",
"exp": "1999999999",
"chainAuthorizations": [
{
"index": 0,
"chainId": "84532",
"status": "success",
"txid": "0x...",
"hash": "0x..."
}
]
}
```
***
## GET /actions/:actionId/intents/:intentIndex/steps/:stepIndex
Retrieve status of a specific chain authorization step.
### Path Parameters
| Parameter | Type | Required | Description |
| ------------- | -------- | -------- | ---------------------- |
| `actionId` | `string` | Yes | Action ID |
| `intentIndex` | `number` | Yes | Intent index (0-based) |
| `stepIndex` | `number` | Yes | Step index (0-based) |
### Response (200)
```json theme={null}
{
"intentId": "act_abc123.../0",
"index": 0,
"chainId": "84532",
"mode": "7702",
"status": "success",
"hash": "0x...",
"txid": "0xTransactionHash...",
"txSentAt": "2024-01-15T10:30:00.000Z",
"txConfirmedAt": "2024-01-15T10:30:15.000Z"
}
```
### Step Status Values
| Status | Description |
| ------------ | ----------------------------------------- |
| `created` | Step created, awaiting execution |
| `registered` | Hash registered (non-7702:on-demand only) |
| `executing` | Transaction sent, awaiting confirmation |
| `success` | Step completed successfully |
| `reverted` | Transaction reverted on-chain |
| `error` | Execution error occurred |
***
## Error Responses
### 400 Bad Request
```json theme={null}
{
"error": "invalid params",
"issues": { /* Validation errors */ }
}
```
### 404 Not Found
```json theme={null}
{
"error": "could not find action",
"actionId": "act_invalid..."
}
```
```json theme={null}
{
"error": "could not find intent",
"actionId": "act_abc123...",
"intentIndex": 5
}
```
```json theme={null}
{
"error": "could not find step",
"actionId": "act_abc123...",
"intentIndex": 0,
"stepIndex": 10
}
```
# Airdrop
Source: https://spiceflow-docs.spicenet.io/api-reference/endpoint/airdrop
Request testnet token airdrops
## Endpoint
```http theme={null}
POST /airdrop
```
Request one or more testnet token airdrops in a single call. Testnet only.
## Request Body
```json theme={null}
{
"airdrops": [
{ "chainId": 84532, "tokenId": "spiceUsd", "amount": "200000000000000000000" }
],
"tag": "optional-label"
}
```
| Field | Type | Required | Description |
| -------------------- | -------- | -------- | ----------------------------------- |
| `airdrops` | `array` | Yes | Non-empty array of airdrop requests |
| `airdrops[].chainId` | `number` | Yes | Target chain ID |
| `airdrops[].tokenId` | `string` | Yes | Token identifier (see below) |
| `airdrops[].amount` | `string` | Yes | Amount in base units, as a string |
| `tag` | `string` | No | Optional label for the batch |
## Supported Token IDs
| Token ID | Description | Example chains |
| ---------- | ----------- | ----------------------------------------------- |
| `spiceUsd` | Test USD | Base Sepolia (84532), Arbitrum Sepolia (421614) |
| `mWBTC` | Test WBTC | Testnet chains |
Verified example token addresses for `spiceUsd`: Base Sepolia `0xf370dC3765f81aC9dD2FEBd59Fb4e710330B0BC8`,
Arbitrum Sepolia `0xBeB51deb2018b67b35d5695Fd15bb30D452c7868`.
## Response
**Success (200)** returns the per-request results (amount airdropped and the airdrop transaction hash for
each entry).
## Error Responses
**400 Bad Request** (validation), for example:
```json theme={null}
{ "success": false, "error": { "message": "airdrops must be a non-empty array" } }
```
```json theme={null}
{ "success": false, "error": { "message": "airdrops[0].tokenId unknownToken is not supported" } }
```
## Example
```bash theme={null}
curl -X POST https://tx-submission-api.spicenet.io/airdrop \
-H "Content-Type: application/json" \
-d '{ "airdrops": [ { "chainId": 84532, "tokenId": "spiceUsd", "amount": "200000000000000000000" } ] }'
```
## Whitelist
A separate `POST /airdrop/whitelist` endpoint records a wallet against a tag:
```bash theme={null}
curl -X POST https://tx-submission-api.spicenet.io/airdrop/whitelist \
-H "Content-Type: application/json" \
-d '{ "wallet": "0x...", "tag": "campaign-x" }'
```
## Notes
* Testnet only, not available on mainnet.
# Health Check
Source: https://spiceflow-docs.spicenet.io/api-reference/endpoint/health
Check API server status
# GET /health
Simple health check endpoint to verify the API server is running.
## Endpoint
```http theme={null}
GET /health
```
## Response
**Success (200)**
```json theme={null}
{
"success": true,
"message": "tx-submission-api server running...",
"gitSha": "abc123def456..."
}
```
| Field | Type | Description |
| --------- | --------- | ---------------------------------- |
| `success` | `boolean` | Always `true` if server is healthy |
| `message` | `string` | Status message |
| `gitSha` | `string` | Git commit SHA of deployed version |
## Example
```bash theme={null}
curl /health
```
## Use Cases
* **Monitoring**: Health check for load balancers and uptime monitoring
* **Debugging**: Verify which version is deployed via `gitSha`
# GET /intent/{intentId}/step/{stepId}/status
Source: https://spiceflow-docs.spicenet.io/api-reference/endpoint/intent-step-status
Get the status for a specific intent step and auto-advance behavior
# GET /intent//step//status
**Deprecated.** This route is not served by the current relayer. Get step status at
[`GET /actions/{actionId}/intents/{intentIndex}/steps/{stepIndex}`](/api-reference/endpoint/actions-execute)
instead. This page is kept for historical reference only.
Returns the status of a specific chain batch (step) for an intent. If the step is in `created` state, the server triggers execution asynchronously. If the step is `success` or `reverted`, the server also triggers the next step.
## Endpoint
```http theme={null}
GET /intent/{intentId}/step/{stepId}/status
```
## Path Parameters
| Parameter | Type | Required | Description |
| ---------- | -------- | -------- | --------------------------------------------- |
| `intentId` | `string` | Yes | The unique intent identifier |
| `stepId` | `number` | Yes | The chain batch index \[0, n) for this intent |
## Response
### Success Response (200)
```json theme={null}
{
"success": true,
"data": {
"status": "created | executing | success | reverted | error",
"transactionHash": "0x...",
"intentId": "0x..."
}
}
```
* `transactionHash` is present when available (e.g., after a transaction is sent).
### Not Found (404)
```json theme={null}
{
"success": false,
"error": { "message": "Chain authorization step not found" }
}
```
## Notes
* When `status` is `created`, the server will start execution for this step in the background.
* When `status` is `success` or `reverted`, the server will attempt to execute the next step.
* Use this endpoint to poll for progress between executions.
# POST /intent/{intentId}/step/{stepId}
Source: https://spiceflow-docs.spicenet.io/api-reference/endpoint/intent-steps
Execute a specific intent step and return the execution results
# POST /intent//execute/
**Deprecated.** This route is not served by the current relayer. Execute a step at
[`POST /actions/{actionId}/intents/{intentIndex}/steps/{stepIndex}`](/api-reference/endpoint/actions-execute)
instead. This page is kept for historical reference only.
Execute a specific intent step and return the execution results.
## Endpoint
```http theme={null}
POST /intent/{intentId}/execute/{stepId}
```
## Description
This endpoint executes a specific step in an intent workflow. Each call performs one unit of work, triggering checks, sending transactions, and returning the execution results. This is an active execution endpoint that modifies state and performs side effects.
## Path Parameters
| Parameter | Type | Required | Description |
| ---------- | -------- | -------- | --------------------------------------------------------------- |
| `intentId` | `string` | Yes | The unique intent identifier (signature of chainBatches) |
| `stepId` | `number` | Yes | The step index \[0, n) representing the chain batch array index |
## Request Body
```json theme={null}
{
"executeOptions": {
"dryRun": false,
"gasLimit": 500000
}
}
```
### Request Parameters
| Field | Type | Required | Description |
| ------------------------- | --------- | -------- | ---------------------------------------------- |
| `executeOptions.dryRun` | `boolean` | No | If true, simulate execution without committing |
| `executeOptions.gasLimit` | `number` | No | Gas limit for transaction execution |
## How It Works
* **Intent ID**: The signature of the chainBatches (similar to Solana's approach)
* **Step ID**: Integer representing the index of the chain batch in the array
* **Execution**: Calling this endpoint triggers the actual work for that step
* **Unit of Work**: Each call performs one discrete unit of work in the intent workflow
## Response
### Success Response (200)
```json theme={null}
{
"success": true,
"data": {
"intentId": "0xabcdef1234567890...",
"stepId": 0,
"status": "completed",
"transactionHash": "0x1234567890abcdef...",
"chainId": 1,
"executionResults": {
"success": true,
"gasUsed": 21000
}
}
}
```
### Response Fields
| Field | Type | Description |
| ------------------ | -------- | ------------------------------------------------- |
| `intentId` | `string` | The intent identifier |
| `stepId` | `number` | The executed step index |
| `status` | `string` | Execution status |
| `transactionHash` | `string` | Hash of the execution transaction (if applicable) |
| `chainId` | `number` | Chain ID where the step was executed |
| `executionResults` | `object` | Results of the step execution |
### Step Status
| Status | Description |
| ----------- | ---------------------------------- |
| `pending` | Step is waiting to be executed |
| `executing` | Step is currently being executed |
| `success` | Step executed successfully |
| `error` | Step execution failed |
| `reverted` | Step transaction reverted on-chain |
| `skipped` | Step was skipped due to conditions |
## Error Responses
### 400 Bad Request
```json theme={null}
{
"success": false,
"error": {
"message": "Invalid stepId: must be integer between 0 and n-1"
}
}
```
### 404 Not Found
Note: The POST execution endpoint currently returns 500 for most failures. Use the GET status endpoint for 404 (not found) semantics.
### 500 Internal Server Error
```json theme={null}
{
"success": false,
"error": {
"message": "Failed to execute intent step",
"code": "EXECUTION_ERROR"
}
}
```
## Example Request
```bash theme={null}
curl -X POST /intent/0xabcdef1234567890.../execute/0 \
-H "Content-Type: application/json" \
-d '{
"executeOptions": {
"dryRun": false,
"gasLimit": 500000
}
}'
```
## Example Usage
```bash theme={null}
# Execute the first step of an intent
curl -X POST /intent/0xabcdef1234567890.../execute/0 \
-H "Content-Type: application/json" \
-d '{"executeOptions": {"dryRun": false}}'
# Response shows execution results
{
"success": true,
"data": {
"intentId": "0xabcdef1234567890...",
"stepId": 0,
"status": "completed",
"transactionHash": "0x1234567890abcdef...",
"chainId": 1,
"executionResults": {
"success": true,
"gasUsed": 21000
}
}
}
# Execute the next step
curl -X POST /intent/0xabcdef1234567890.../execute/1 \
-H "Content-Type: application/json" \
-d '{"executeOptions": {"dryRun": false}}'
# Continue until all steps are complete
```
## Workflow Pattern
1. **Submit Intent**: Use `POST /transaction/submit` to create the intent
2. **Execute Steps**: Call `POST /intent/{id}/execute/{i}` for each step i = 0, 1, 2, ...
3. **Check Results**: Each response contains the execution results for that step
4. **Continue**: Execute steps sequentially until the intent is complete
## Notes
* Steps must be executed in order (0, 1, 2, ...)
* Each step corresponds to a chain batch in the original array
* The endpoint performs actual execution with side effects (not idempotent)
* Failed steps may be retryable depending on the failure reason
* Intent data is persisted in the database for reliable execution tracking
* Use `dryRun: true` to simulate execution without committing changes
# Spice Balances
Source: https://spiceflow-docs.spicenet.io/api-reference/endpoint/spice-deposit
Query a user's Spice balances
## Endpoint
| Method | Endpoint | Description |
| ------ | --------------------------- | --------------------------- |
| `GET` | `/wallets/:address/balance` | Get a user's Spice balances |
Deposits are not recorded through a manual endpoint. A user funds their Spice account through the
deposit flow (the `SpiceDeposit` component, or the escrow-deposit path of a `POST /actions` intent),
and the balance below reflects the result. There is no `POST /spicedeposit`.
***
## GET /wallets/:address/balance
Returns the user's Spice (off-chain) balances: what they have deposited into escrow and can spend on
gasless actions. This is the server ledger, not on-chain wallet balances.
### Path Parameters
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ------------------- |
| `address` | `string` | Yes | User wallet address |
### Query Parameters
| Parameter | Type | Required | Description |
| -------------- | -------- | -------- | ----------------------------------- |
| `receiptToken` | `string` | No | Filter to specific receipt token(s) |
| `chainToken` | `string` | No | Filter to specific chain token(s) |
### Response
**Success (200)**
```json theme={null}
{
"balances": [
{
"receiptToken": "0x...",
"token": "0x...",
"chainId": 8453,
"amount": "1000000000000000000"
}
]
}
```
Each entry is one balance line; `amount` is a string in the token's base units.
### Example
```bash theme={null}
curl https://tx-submission-api.spicenet.io/wallets/0x742d35Cc6634C0532925a3b844e4B7db0D6d8E5c/balance
```
The SDK exposes this through `useSpiceAssets` and the exported `fetchBalances` / `useWalletBalances`
helpers, which honour the configured `apiUrl`. Prefer those over calling the endpoint directly.
# POST /transaction/submit
Source: https://spiceflow-docs.spicenet.io/api-reference/endpoint/transaction-submit
Submit a cross-chain transaction with EIP-7702 authorization and intent execution
# POST /transaction/submit
**Deprecated.** This endpoint is not served by the current relayer. Use
[`POST /actions`](/api-reference/endpoint/actions-create) instead, which is the unified entry point for
creating intents. This page is kept for historical reference only.
Submit a cross-chain transaction with EIP-7702 authorization and intent execution.
## Endpoint
```http theme={null}
POST /transaction/submit
```
## Description
This endpoint allows you to submit transactions that will be executed across multiple chains. The system validates the transaction, saves it to the database, and returns tracking information. Execution is performed asynchronously by a solver.
## Request Body
```typescript theme={null}
{
address: Address; // User wallet address
authorization: [{
chainId: number;
address: string;
nonce: number;
r: string;
s: string;
yParity: number;
}];
intentAuthorization: {
signature: string;
chainBatches: Array<{
hash: string;
chainId: number | bigint;
calls: Array<{ to: string; value: string | number | bigint; data: string }>;
recentBlock?: number | bigint;
}>;
};
tokenTransfers: Array>;
}
```
### Parameters
| Field | Type | Required | Description |
| ---------------------------------- | -------- | -------- | --------------------------------------- |
| `address` | `string` | Yes | User wallet address |
| `authorization` | `array` | Yes | Array of EIP-7702 authorization objects |
| `authorization[].chainId` | `number` | Yes | Chain ID for the authorization |
| `authorization[].address` | `string` | Yes | Address being authorized |
| `authorization[].nonce` | `number` | Yes | Authorization nonce |
| `authorization[].r` | `string` | Yes | ECDSA signature R value |
| `authorization[].s` | `string` | Yes | ECDSA signature S value |
| `authorization[].yParity` | `number` | Yes | ECDSA signature Y parity |
| `intentAuthorization.signature` | `string` | Yes | Intent signature (becomes intent ID) |
| `intentAuthorization.chainBatches` | `array` | Yes | Array of chain batches with calls |
| `tokenTransfers` | `array` | Yes | 2D array of transfers per chain batch |
## Response
### Success Response (200)
```json theme={null}
{
"success": true,
"data": {
"status": "created",
"intentId": "0xabcdef1234567890...",
"transactionId": "uuid-generated-id"
}
}
```
### Response Fields
| Field | Type | Description |
| --------------- | -------- | --------------------------------------------------------- |
| `status` | `string` | Submission status (always `created` on success) |
| `intentId` | `string` | Intent identifier (same as intentAuthorization.signature) |
| `transactionId` | `string` | Generated transaction ID for tracking |
## Error Responses
### 400 Bad Request
```json theme={null}
{
"success": false,
"error": {
"message": "Missing authorization or intentAuthorization"
}
}
```
```json theme={null}
{
"success": false,
"error": {
"message": "Token transfers do not match calls"
}
}
```
The API validates that `tokenTransfers` match the `calls` in each chain batch. Each transfer must correspond to the expected call data.
### 500 Internal Server Error
```json theme={null}
{
"success": false,
"error": {
"message": "Internal server error"
}
}
```
## Example Request
```bash theme={null}
curl -X POST /transaction/submit \
-H "Content-Type: application/json" \
-d '{
"authorization": [
{
"chainId": 1,
"address": "0x742d35Cc6634C0532925a3b8D8D428C83c76a4B",
"nonce": 42,
"r": "0x1234567890abcdef...",
"s": "0xfedcba0987654321...",
"yParity": 1
},
{
"chainId": 137,
"address": "0x742d35Cc6634C0532925a3b8D8D428C83c76a4B",
"nonce": 43,
"r": "0xabcdef1234567890...",
"s": "0x0987654321fedcba...",
"yParity": 0
}
],
"intentAuthorization": {
"signature": "0xabcdef1234567890...",
"chainBatches": [
{
"hash": "0x9876543210fedcba...",
"chainId": 1,
"calls": [
{
"to": "0x123...abc",
"value": 0,
"data": "0xa9059cbb..."
}
],
"recentBlock": 18500000
}
]
}
}'
```
## Workflow
1. **Transaction Validation**: Validates authorization array and intentAuthorization are provided and properly formatted
2. **Database Storage**: Saves the intent and all chain authorizations to the database for reliable tracking
3. **Chain Broadcast**: Broadcasts the transaction to the appropriate blockchain network
4. **Response**: Returns transaction hash and intent ID for subsequent step execution
## Next Steps
After successful submission:
1. **Track Progress**: Use the returned `intentId` to execute individual steps
2. **Execute Steps**: Call `POST /intent/{intentId}/execute/{stepId}` for each step (0, 1, 2, ...)
3. **Monitor Results**: Each step execution returns detailed results and status
## Example Workflow
```bash theme={null}
# 1. Submit the intent with multiple authorizations
curl -X POST /transaction/submit -d '{
"authorization": [
{"chainId": 1, "address": "0x742...", "nonce": 42, ...},
{"chainId": 137, "address": "0x742...", "nonce": 43, ...}
],
"intentAuthorization": {...}
}'
# Returns: {"success": true, "data": {"transactionHash": "0x123...", "intentId": "0xabc..."}}
# 2. Execute step 0
curl -X POST /intent/0xabc.../execute/0 -d '{"executeOptions": {...}}'
# Returns: execution results for first chain authorization
# 3. Execute step 1
curl -X POST /intent/0xabc.../execute/1 -d '{"executeOptions": {...}}'
# Returns: execution results for second chain authorization
# Continue for all steps...
```
## Notes
* Intent ID is derived from `intentAuthorization.signature`
* Authorization array can contain multiple EIP-7702 authorizations for different chains/addresses
* All data is persisted to database for reliability and recovery
* Step execution is triggered separately via the intent execution endpoint
* Failed submissions can be debugged using the transaction hash
* The system supports resumable execution for complex multi-step intents
# Handle Transaction
Source: https://spiceflow-docs.spicenet.io/api-reference/endpoint/venue-tx-hashes
Register an on-chain transaction hash for the solver to process
## Endpoint
```http theme={null}
POST /transactions/handle/evm
```
Register an EVM transaction hash so the relayer inspects its receipt and records the relevant transfers
(for example, an escrow deposit) against the user's Spice balance. This replaces the older
`/venue-tx-hashes` endpoints.
## Request Body
```json theme={null}
{
"chainId": 8453,
"hash": "0x..."
}
```
| Field | Type | Required | Description |
| --------- | -------- | -------- | ---------------------------------- |
| `chainId` | `number` | Yes | Chain the transaction was mined on |
| `hash` | `Hex` | Yes | Transaction hash |
## Response
**Success (200)** returns the processing result. The relayer fetches the transaction receipt on the
given chain, filters the relevant transfer logs, and records them.
## Example
```bash theme={null}
curl -X POST https://tx-submission-api.spicenet.io/transactions/handle/evm \
-H "Content-Type: application/json" \
-d '{ "chainId": 8453, "hash": "0x..." }'
```
The old `POST/GET /venue-tx-hashes` endpoints are no longer served. Use `POST /transactions/handle/evm`.
# Delegate Contract
Source: https://spiceflow-docs.spicenet.io/deep-dive/delegate
A technical overview of our delegate contract, the core of the EIP-7702 implementation responsible for executing user intents on-chain.
## The `Delegate` Contract: The On-Chain Execution Engine
The `Delegate` contract is the on-chain component that receives and executes the user's signed instructions. It is the implementation that an EOA delegates its authority to. It acts as the user's temporary agent on the target chain, ensuring that only the actions specified in the user's signed `Intent` are executed.
### Key Functions and Concepts
* **`Intent` and `ChainBatch` Structs**: These structs define the core data structure. An `Intent` is a collection of `ChainBatch`es, and each batch is a set of calls for a specific `chainId`. This is how the contract can handle multi-chain instructions.
* **`execute(Intent calldata intent)`**: This is the primary function for executing a user's signed intent. The most critical part is the signature verification: `if (recovered != address(this)) revert InvalidSignature(recovered, address(this));`. When a user's EOA is delegated to this contract, `address(this)` *is* the user's address for the duration of the transaction. This check ensures that the person who signed the intent is the same person whose authority is being used to execute it.
* **`selfExecute(Call[] calldata calls)`**: This function is what the solver calls. The key here is the `if (msg.sender != address(this)) revert InvalidAuthority();` check. The solver submits a transaction that includes the user's EIP-7702 authorization. This authorization makes the solver's EOA *become* the `Delegate` contract. Therefore, when the solver calls `selfExecute` on itself, the `msg.sender` is `address(this)`, and the check passes. This allows the solver to bundle the user's intent with other calls, like fee payments or refunding the user.
* **Replay Protection (`_registerSignatureHash`)**: To prevent a malicious actor from re-submitting an old, completed transaction, the contract keeps a history of recently executed `ChainBatch` hashes. It stores these hashes in buckets corresponding to the `recentBlockNumber` provided in the batch. It will revert if it finds the same hash has been used in a recent block, preventing double-spending and replay attacks. The history is limited to `16` blocks to keep storage costs manageable.
* **Timeliness Checks**: The contract also ensures that a `ChainBatch` is executed within a reasonable time window. It reverts if the `recentBlock` is in the future (`TooEarly`) or more than 16 blocks in the past (`SignatureExpired`).
### Overall Flow
# EIP-7702: Account Abstraction via Temporary Delegation
Source: https://spiceflow-docs.spicenet.io/deep-dive/eip7702
Technical deep dive into EIP-7702 and how Spice Flow leverages EIP-7702-powered delegation for atomic transaction execution.
## EIP-7702: Temporary Account Delegation
**EIP-7702** is a new and powerful Ethereum Improvement Proposal, which came was first created in May 2024, that allows an *Externally Owned Account (EOA)* (a regular user wallet) to temporarily act as a smart contract on EVM chains. It achieves this by letting a user sign an `authorization` that delegates their account's authority to a specific smart contract implementation.
**Why is this important?**
Instead of a user sending multiple transactions across different chains, they can sign a single **EIP-7702** `authorization` for each chain. This authorization is like giving a one-time permission slip to our system to perform specific actions on that chain. It's powerful because:
* **It's Non-custodial**: The user never gives up control of their private keys. The authorization is a signature, not a key transfer.
* **It's Secure**: The authorization is for a specific contract and is only valid for one chain, minimizing risk.
* **It's atomic**: The user can bundle multiple actions into a single intent, which is then executed as a single transaction on each chain, if all actions are valid, if any action fails, the entire transaction reverts. For example, a user can sign an intent that includes swapping tokens on one chain and bridging assets to another chain. If either action fails, neither action is executed, ensuring the user doesn't end up in a partial state.
* **It Enables Gas Sponsorship**: Because the user delegates execution, a third-party *"solver"* can submit the transaction and pay the gas fees on the user's behalf.
* **It Enables Batching**: A solver can bundle multiple actions into a single on-chain transaction. For example, executing a user's intent *and* paying the user back for their initial deposit.
* **It Allows Privileged Execution**: The contract can perform actions that a regular EOA cannot, such as interacting with other smart contracts, executing complex logic, or even paying gas fees in tokens other than ETH.
* **It's compatible with EIP-4337**: EIP-7702 can be used in conjunction with EIP-4337 (Account Abstraction) to provide a seamless user experience. For example, a user could sign an EIP-7702 authorization that delegates to an EIP-4337 smart contract wallet, allowing for even more complex interactions.
## Some specifications
### Parameters
| Parameter | Value |
| ------------------------ | ------- |
| `SET_CODE_TX_TYPE` | `0x04` |
| `MAGIC` | `0x05` |
| `PER_AUTH_BASE_COST` | `12500` |
| `PER_EMPTY_ACCOUNT_COST` | `25000` |
A new **EIP-2718** transaction known as the "set code transaction" is introduced with **EIP-7702**, where the *TransactionType* is set to `0x04` and *TransactionPayload* is the RLP serialization of the following:
```
rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
destination, value, data, access_list, authorization_list, signature_y_parity,
signature_r, signature_s])
authorization_list = [[chain_id, address, nonce, y_parity, r, s], ...]
```
* the fields inside the `rlp` of the outer transaction follow the same semantics as *EIP-4844*.
* The `signature_y_parity, signature_r, signature_s` elements of this transaction represent a *secp256k1* signature over `keccak256(SET_CODE_TX_TYPE || TransactionPayload)`.
* `authorization_list` is a list of authorizations, each containing:
* `chain_id`: the chain ID where the authorization is valid.
* `address`: the address of the contract to which authority is being delegated.
* `nonce`: a unique number to prevent replay attacks.
* `y_parity`, `r`, `s`: components of the ECDSA signature.
* The authorization\_list is a list of tuples that indicate what code the signer of each tuple desires to execute in the context of their EOA. The transaction is considered invalid if the length of authorization\_list is zero.
## Behavior
The authorization list is processed before the execution of the transaction, but after the sender's nonce is incremented.
For each `[chain_id, address, nonce, y_parity, r, s]` tuple, perform the following:
1. Verify the chain ID is 0 or the ID of the current chain.
2. Verify the `nonce` is less than `2**64 - 1`.
3. Let `authority = ecrecover(msg, y_parity, r, s)`.
* Where `msg = keccak(MAGIC || rlp([chain_id, address, nonce]))`.
* Verify `s` is less than or equal to `secp256k1n/2`, as specified in EIP-2.
4. Add `authority` to `accessed_addresses`, as defined in EIP-2929.
5. Verify the code of `authority` is empty or already delegated.
6. Verify the nonce of `authority` is equal to `nonce`.
7. Add `PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST` gas to the global refund counter if `authority` is not empty.
8. Set the code of `authority` to be `0xef0100 || address`. This is a delegation indicator.
* If `address` is `0x0000000000000000000000000000000000000000`, do not write the delegation indicator. Clear the account’s code by resetting the account’s code hash to the empty code hash `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`.
9. Increase the nonce of `authority` by one.
In case, any step fails, the processing of that tuple is halted immediately, and skipped to the next tuple in the authorization list.
Note, if transaction execution results in failure (e.g. any exceptional condition or code reverting), the processed delegation indicators is not rolled back.
***
### Sources
* [https://eip7702.io/](https://eip7702.io/)
* [https://eips.ethereum.org/EIPS/eip-7702](https://eips.ethereum.org/EIPS/eip-7702)
# Best Practices
Source: https://spiceflow-docs.spicenet.io/guides/best-practices
Security guidelines, common pitfalls, and best practices for developers integrating with Spice Flow
# Best Practices
This document outlines best practices, common pitfalls, and security considerations for developers integrating with the Transaction Submission API. Following these guidelines will help ensure a secure, efficient, and reliable integration.
## General Principles
* **Validate Inputs**: Always validate and sanitize any inputs from users or external systems that are used to construct calls within an intent. Never trust user input directly when forming transaction data.
* **Least Privilege**: Construct intents with the minimum permissions necessary to achieve the desired outcome. For example, if swapping tokens, set the exact amount for approval rather than an unlimited approval.
* **Idempotency**: Design your submission logic to be idempotent where possible. If you encounter a network error when submitting an intent, you should be able to safely retry the submission without creating duplicate intents.
## Security Best Practices
* **Signature Verification**: The security of the entire system relies on cryptographic signatures. Ensure you are using trusted libraries (like `viem` or `ethers`) to produce EIP-7702 authorizations and intent signatures.
* **Protect User Private Keys**: In any front-end integration, the user's private key must remain entirely within their control. Your application should only ever request signatures for authorizations and intents, never the key itself.
* **Chain ID and Contract Address Validation**: Double-check that you are targeting the correct `chainId` and `delegate` contract address when creating EIP-7702 authorizations. A mismatched chain ID can lead to failed transactions or unexpected errors.
## Common Errors and Troubleshooting
* **`CallReverted`**: This means one of the calls within your `ChainBatch` failed on-chain.
* **Troubleshooting**:
1. Check the `to`, `value`, and `data` for the failing call. Is the address correct? Is the function signature in the data correct?
2. Ensure the `Delegate` contract has the necessary approvals (e.g., ERC20 `approve`) to perform the action on behalf of the user. Approvals should be included as one of the first calls in your `ChainBatch`.
## Integration Guidelines
### Frontend Integration
* Use established Web3 libraries like `viem`, `ethers`, or `wagmi` for wallet connections and signature generation
* Always show users what they're signing before requesting signatures
* Implement proper loading states during transaction submission and execution
* Handle network errors gracefully with retry mechanisms
### Backend Integration
* Implement proper error handling and logging for all API calls
* Use database transactions when updating your application state based on intent execution
* Consider implementing webhooks or polling for intent status updates
* Store transaction hashes for audit trails and user support
### Performance Optimization
* Batch multiple operations into single intents when possible
* Use appropriate gas limits to avoid failed transactions
* Consider implementing retry logic with exponential backoff for network errors
* Cache frequently accessed data like chain configurations
## Development Workflow
1. **Test on Testnets**: Always test your integration thoroughly on testnets before going to production
2. **Use Dry Runs**: Utilize the `dryRun` option in step execution to validate your intents before committing
3. **Monitor Gas Usage**: Track gas consumption patterns to optimize your intents
4. **Handle Edge Cases**: Plan for scenarios like network congestion, failed steps, and partial execution
## Security Checklist
* [ ] User private keys never leave their device
* [ ] All user inputs are validated and sanitized
* [ ] Chain IDs and contract addresses are verified
* [ ] Signature verification is implemented correctly
* [ ] Error handling doesn't leak sensitive information
* [ ] Rate limiting is implemented to prevent abuse
* [ ] Audit trails are maintained for all transactions
## Support and Resources
Technical details about EIP-7702 delegation
Complete implementation example
React.js integration guide
Understanding the technical flow
# Custom UI Integration
Source: https://spiceflow-docs.spicenet.io/guides/custom-ui
Fund any market with any asset from any chain, using your own components
Use this guide when you want your own UI — asset selector, amount input, buttons, status — and the SDK
only as the execution engine. This is the pattern our live production integrations use. The
prebuilt modals (`SpiceSupply`, `SpiceDeposit`) package this same pipeline; use them instead if you
want less code.
The pattern, per market:
1. Describe the market's token as a `destinationToken`.
2. Write one `buildActionCalls` function returning the market's on chain calls (approve + supply).
3. `useSupplyAssets` lists every asset the user can fund it with, across all supported chains.
4. `useSupplyQuote` resolves routing (swap needed or direct) for the selected source asset.
5. `estimateFeePreview` produces the fee quote — surface it before enabling the submit button.
6. `executeSupply` runs the whole flow: funding, routing, delegation signing, solver execution.
The user never bridges and never needs gas on the source chain — the Spicenet solver executes via
EIP 7702 and covers gas on the routing legs, accounted for in the fee quote. If your app already
sponsors gas on the destination chain (e.g. Privy gas sponsorship on Base), keep it: it applies to
your app's own transactions and needs no SDK configuration.
## Provider Setup
`SpiceFlowProvider` does not mount Privy, wagmi, or react-query — it reads wallet state from those
contexts. Your app mounts all four, in this order:
```tsx theme={null}
import "@spicenet-io/spiceflow-ui/styles.css"; // required once, at app entry
import type { ReactNode } from "react";
import { WagmiProvider, createConfig, http } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PrivyProvider } from "@privy-io/react-auth";
import { SpiceFlowProvider } from "@spicenet-io/spiceflow-ui";
import { getMainnetChains } from "@spicenet-io/spiceflow-core";
import { defineChain, type Chain } from "viem";
// Every chain the installed SDK supports; new chains arrive with SDK upgrades.
const chains = getMainnetChains().map((c): Chain =>
defineChain({
id: c.id,
name: c.displayName,
nativeCurrency: c.nativeCurrency,
rpcUrls: { default: { http: [c.rpcUrl] } },
blockExplorers: { default: { name: "Explorer", url: c.blockExplorer } },
}),
) as [Chain, ...Chain[]];
const wagmiConfig = createConfig({
chains,
transports: Object.fromEntries(chains.map((c) => [c.id, http()])),
});
const queryClient = new QueryClient();
export function Providers({ children }: { children: ReactNode }) {
return (
{/* supportedChainIds omitted: SpiceFlowProvider defaults to every supported chain */}
{children}
);
}
```
One `chains` array feeds wagmi, Privy, and the SDK, so the three stay consistent by construction.
If you restrict to specific chains instead, keep all three in sync — a chain missing from the
`PrivyProvider` config fails at wallet-client creation with "Chain X is not configured in
PrivyProvider". If you already run Privy, reuse your existing `PrivyProvider` — just add the source
chains you enable. For production, put your own RPC URLs in the wagmi `transports` — the bare
`http()` falls back to each chain's public endpoint, which rate-limits under real traffic.
The `styles.css` import is required even with a fully custom UI — the Privy login modal renders
unstyled (invisible) without it. On Vite, also polyfill Node's `Buffer` global before the SDK loads;
cross-chain swap quoting references it at runtime.
## Complete Example
A supply panel for one market. Verified against `@spicenet-io/spiceflow-ui@4.7.9`.
```tsx theme={null}
"use client";
import { useRef, useState } from "react";
import { type Address } from "viem";
import {
useWallet,
useSpiceExecution,
useSupplyAssets,
useSupplyQuote,
buildSupplyChainBatches,
buildPayChainBatches,
estimateFeePreview,
executeSupply,
type Asset,
type BatchBuilderContext,
type BuildActionCallsContext,
type Call,
type ExecutorContext,
type SupplyStep,
type SupplyQuoteResult,
} from "@spicenet-io/spiceflow-ui";
const destinationToken = {
chainId: 8453,
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as Address, // USDC on Base
symbol: "USDC",
decimals: 6,
};
// One function per market: the destination chain calls to run once funds arrive.
// ctx.userAddress is the wallet the action runs as.
const buildActionCalls = (amount: bigint, ctx: BuildActionCallsContext): Call[] => [
{ to: destinationToken.address, value: 0n, data: encodeApprove(MARKET, amount) },
{ to: MARKET, value: 0n, data: encodeSupply(destinationToken.address, amount, ctx.userAddress) },
];
export function SupplyPanel() {
const { address, isReady, isAuthenticated, isConnected, provider } = useWallet();
const { execute, estimate } = useSpiceExecution();
// Every asset the user can fund this market with, across all chains
const { displayAssets, embeddedWalletAddress, externalAddress, isNon7702 } =
useSupplyAssets({ address, destinationToken, enabled: true });
const [selected, setSelected] = useState(null);
const [amount, setAmount] = useState("");
const [step, setStep] = useState("idle");
const [error, setError] = useState(null);
const [statusMsg, setStatusMsg] = useState("");
const [txHash, setTxHash] = useState(null);
const [isExecuting, setIsExecuting] = useState(false);
const [, setShowStatusPanel] = useState(false);
const [, setPaymentResult] = useState(null);
const submittedRef = useRef(false);
const abortRef = useRef(null);
const selectedAsset = selected ? { asset: selected, amount } : null;
// Routing quote: does the chosen source asset need a swap into the destination token?
const quote: SupplyQuoteResult = useSupplyQuote({
direction: "input",
selectedAsset,
destinationToken,
chainId: destinationToken.chainId,
recipient: embeddedWalletAddress ?? address,
paymentAmount: amount,
});
// The token the execution is funded in on the destination chain
const executionTokenAddress: Address =
quote.needsSwap && quote.resolvedTokenIn
? quote.resolvedTokenIn
: selected && selected.chainId === destinationToken.chainId
? selected.address
: (quote.resolvedTokenIn ?? destinationToken.address);
const isSourceDestToken =
!!selected &&
selected.address.toLowerCase() === destinationToken.address.toLowerCase() &&
selected.chainId === destinationToken.chainId;
const supply = async () => {
if (!selectedAsset) return;
const batchCtx: BatchBuilderContext = {
currentPaymentAmount: amount,
destinationToken,
embeddedWalletAddress,
providerEmbeddedAddress: embeddedWalletAddress,
externalWalletAddress: externalAddress,
address,
isNon7702,
isPayMode: false,
resolvedChainId: destinationToken.chainId,
buildActionCalls,
feeExecutionMode: "backend-transfer",
buildSpicenetBatch: undefined,
selectedAsset,
needsSwap: quote.needsSwap,
resolvedSwap: quote.resolvedSwap,
exactOutputSwap: quote.exactOutputSwap,
quoteAmountOut: quote.quoteAmountOut,
resolvedTokenIn: quote.resolvedTokenIn,
requestTimestampMs: Date.now(),
};
const buildSupply = (opts?: Parameters[1]) =>
buildSupplyChainBatches(batchCtx, opts);
const buildPay = (opts?: Parameters[1]) =>
buildPayChainBatches(batchCtx, opts);
const parsedAmount = parseFloat(amount) || 0;
// Fee quote. In production, run this when the amount settles and show it
// before enabling the submit button.
const feeQuote = await estimateFeePreview({
selectedAsset,
isPayMode: false,
parsedAmount,
needsSwap: quote.needsSwap,
exactOutputSwap: quote.exactOutputSwap,
isNon7702,
isSourceDestToken,
currentPaymentAmount: amount,
destinationToken,
feeExecutionMode: "backend-transfer",
resolvedTokenIn: quote.resolvedTokenIn,
embeddedWalletAddress,
providerEmbeddedAddress: embeddedWalletAddress,
resolvedChainId: destinationToken.chainId,
isConnected,
address,
ready: isReady,
authenticated: isAuthenticated,
isDirect: quote.isDirect,
resolvedSwap: quote.resolvedSwap,
isQuoting: quote.isQuoting,
executionTokenAddress,
buildPayChainBatches: buildPay,
buildSupplyChainBatches: buildSupply,
estimate,
});
const ctx: ExecutorContext = {
selectedAsset,
parsedAmount,
currentPaymentAmount: amount,
isInsufficientBalance: parsedAmount > (selected?.balanceFormatted ?? 0),
isPayMode: false,
isNon7702,
isSourceDestToken,
needsSwap: quote.needsSwap,
exactOutputSwap: quote.exactOutputSwap,
selectedIsEquivalent: quote.selectedIsEquivalent,
resolvedTokenIn: quote.resolvedTokenIn,
embeddedWalletAddress,
providerEmbeddedAddress: embeddedWalletAddress,
externalWallet: null,
externalWalletAddress: externalAddress,
address,
isConnected,
ready: isReady,
authenticated: isAuthenticated,
provider,
resolvedChainId: destinationToken.chainId,
executionTokenAddress,
feeQuote,
execution: {
estimate,
execute,
buildPayChainBatches: buildPay,
buildSupplyChainBatches: buildSupply,
onProgress: (p) => setStatusMsg(p.message),
},
ui: {
setError,
setStep,
setStatusMsg,
setTxHash,
setIsExecuting,
setShowStatusPanel,
setPaymentResult,
},
refs: {
spiceDepositSubmittedRef: submittedRef,
abortControllerRef: abortRef,
},
destinationToken,
buildActionCalls,
feeExecutionMode: "backend-transfer",
buildSpicenetBatch: undefined,
onPayExecute: undefined,
onSuccess: (hash) => console.log("supplied", hash),
};
await executeSupply(ctx);
};
return (
);
}
```
Everything above the JSX is wiring; the JSX is yours to replace entirely.
## Scaling to All Markets
Only two things vary per market: `destinationToken` and `buildActionCalls`. Wrap the panel in a
component that takes those as props (or a market config object) and every market gets any-asset,
any-chain funding with the same code path.
## Withdrawals
`SpiceWithdraw` handles withdrawing Spice balances back to a wallet on any supported chain. To build
your own withdraw UI instead, use `useSpiceAssets` for balances and `useSpiceExecution` for
execution — see [Hooks](/sdk/hooks).
## Testing
Set `network="testnet"` on `SpiceFlowProvider` (and swap the wagmi/Privy chain set to the testnet
chains) to run the identical flow against testnet infrastructure. Testnet funds for supported tokens
come from the [airdrop endpoint](/api-reference/endpoint/airdrop). Move to `network="mainnet"` only
after the full supply → withdraw loop passes on testnet.
## Notes
* `feeExecutionMode: "backend-transfer"` lets the backend collect fees from the funded amount; the
fee quote's `netAmount` is what reaches the market.
* `executeSupply` drives your state through the `ui` setters — render `step`, `statusMsg`, and
`error` however you like. Steps: `idle → signing-delegation → signing-intent → submitting →
executing → success | error`.
* Supported chains come from the SDK per `network` — see
[Configuration](/sdk/configuration#default-chains-by-network). Restrict with `supportedChainIds`
or `sourceChains` on `useSupplyAssets`.
# Native Swap Example
Source: https://spiceflow-docs.spicenet.io/guides/native-swap
Create a simple asset swap on Citrea backed by collateral on Ethereum sepolia
# Guide: Native Swap Example
This guide provides a complete, code-first walkthrough for creating and submitting a swap transaction using the API. We will build a real-world example: swapping USD to cBTC on the Citrea blockchain backed by Sepolia ETH as collateral, using [Crest](https://x.com/crest_btc) as a liquidity source.
## The Goal
Our goal is to create an "intent" that performs the following actions:
1. **On Sepolia Testnet**: A user deposits `0.0001` Sepolia ETH into an escrow EOA.
2. **On Citrea Testnet**: Using the value from the deposit, execute a swap for cBTC via `crest` contract.
3. **Finally**: The solver sends the principal amount of the deposit back to the user on Citrea.
This entire process will be defined in a single intent, signed by the user once, and submitted to the API.
## Prerequisites
You'll need `viem` to interact with chains, create authorizations, and sign messages. For frontend applications, you would need to use `privy` SDK to work with embedded wallets.
```typescript theme={null}
import {
createWalletClient,
http,
parseUnits,
getAddress,
formatEther
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { citreaTestnet, sepolia } from "viem/chains";
```
## Step 1: Setting up Accounts and Clients
First, we define the accounts and blockchain clients we'll be working with. In a real application, the `user` would be the end-user interacting with your application, and their private key would be managed by their wallet.
```typescript theme={null}
// A sample user account
const user = privateKeyToAccount("0x...");
// The escrow contract address where the initial deposit is sent
const escrowAddress = getAddress("0x...");
const publicSepoliaClient = createPublicClient({
chain: sepolia,
transport: http(),
});
const publicCitreaClient = createPublicClient({
chain: citreaTestnet,
transport: http(),
});
```
## Step 2: Define the Intent Parameters
We'll define the amounts and generate a quote for the swap part of our intent.
```typescript theme={null}
const depositAmount = parseUnits("0.0001", 18);
// In a real app, you would call an external service like crest's /rfqt endpoint
// to get a signed quote for the swap.
const crestRfqt = await generateRfqt({
quoteId: "0x...",
user: user.address,
tokenIn: "usd",
tokenOut: "bitcoin",
amountIn: formatEther(depositAmount),
amountOut: "100", // Targeting 100 USD
expiry: Math.floor(Date.now() / 1000) + 60, // 1 minute expiry
});
```
## Step 3: Create EIP-7702 Authorizations
The core of the process is the user granting our system one-time authority on each chain. The user's wallet will sign an EIP-7702 authorization for each `delegate` contract on each chain.
```typescript theme={null}
// Fetch the current nonce for the user on each chain
const nonceSepolia = await publicSepoliaClient.getTransactionCount({address: user.address});
const nonceCitrea = await publicCitreaClient.getTransactionCount({address: user.address});
// Read the delegate contract per chain from the SDK; do not hardcode (it differs per chain).
import { getDelegateContract } from "@spicenet-io/spiceflow-core";
const delegateAddressSepolia = getDelegateContract(sepolia.id);
const delegateAddressCitrea = getDelegateContract(citreaTestnet.id);
// User signs an authorization for Sepolia
const userAuthSepolia = await user.signAuthorization({
address: delegateAddressSepolia,
chainId: sepolia.id,
nonce: nonceSepolia,
});
// User signs another authorization for Citrea
const userAuthCitrea = await user.signAuthorization({
address: delegateAddressCitrea,
chainId: citreaTestnet.id,
nonce: nonceCitrea,
});
```
These `authorization` objects are what a solver will use to execute transactions on the user's behalf.
## Step 4: Construct the Chain Batches
Now we define the specific on-chain actions. Each object in the `chainBatches` array represents a set of calls to be executed on a specific blockchain, in order.
Import the hashing helpers from `@spicenet-io/spiceflow-core`. Do not hand-roll them, and do not add a
`recentBlock` field: the current chain-batch shape is `{ chainId, calls }`, and the request schema
rejects unknown keys.
```typescript theme={null}
import { hashChainBatches } from "@spicenet-io/spiceflow-core";
const chainBatches = hashChainBatches([
// Step 0: Deposit Sepolia ETH to escrow
{
chainId: sepolia.id,
calls: [{ to: escrowAddress, data: "0x", value: depositAmount }],
},
// Step 1: Swap for cBTC on Citrea. The RFQT object from Step 2 is a valid "Call".
{
chainId: citreaTestnet.id,
calls: [crestRfqt],
},
]);
```
`hashChainBatches` returns each batch as `{ hash, chainId, calls }`.
## Step 5: Sign the Intent and Build the Request
The user signs the intent hash. `getIntentHash` takes the signature type, a not-before timestamp
(`nbf`), an expiry (`exp`), and the hashed chain batches:
```typescript theme={null}
import { getIntentHash } from "@spicenet-io/spiceflow-core";
const nbf = 0n;
const exp = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 hour
const digest = getIntentHash("ecdsa", nbf, exp, chainBatches);
const signature = await user.signMessage({ message: { raw: digest } });
```
Assemble the `POST /actions` request body. `chainAuthorizations` are the EIP-7702 authorizations from
Step 3, each `{ r, s, yParity, address, chainId, nonce }` (drop the legacy `v` field). Each chain
batch also carries its `tokenTransfers` (how the solver moves tokens):
```typescript theme={null}
const apiRequest = {
user: user.address,
chainAuthorizations: [userAuthSepolia, userAuthCitrea],
intents: [
{
mode: "7702",
signatureType: "ecdsa",
signature,
nbf: Number(nbf),
exp: Number(exp),
chainBatches: chainBatches.map((b) => ({ ...b, tokenTransfers: [] })),
},
],
};
```
Send it to `POST /actions`. See the [POST /actions](/api-reference/endpoint/actions-create) reference
for the full request and response schema.
```bash theme={null}
curl -X POST https://tx-submission-api.spicenet.io/actions \
-H "Content-Type: application/json" \
-d '{
"user": "0x...",
"chainAuthorizations": [ { "r": "0x...", "s": "0x...", "yParity": 0, "address": "0x...", "chainId": 11155111, "nonce": 0 } ],
"intents": [
{
"mode": "7702",
"signatureType": "ecdsa",
"signature": "0x...",
"nbf": 0,
"exp": 1750000000,
"chainBatches": [ { "hash": "0x...", "chainId": 11155111, "tokenTransfers": [], "calls": [ ... ] } ]
}
]
}'
```
## What Happens Next: The Solver
After you submit the intent, the API makes it available to a network of solvers. A solver will:
1. Pick up your intent.
2. Execute **Step 0** by sending a transaction to the `Delegate` contract on Sepolia, using your signed authorization. This deposits your funds to escrow.
3. Wait for the required block confirmations.
4. Execute **Step 1** by sending a transaction to the `Delegate` contract on Citrea, which performs the swap.
5. As part of the final transaction, the solver also transfers the principal deposit amount (`tokenAmount`) back to your address (`address`) on the destination chain (Citrea).
Your application can track each step using the `GET /actions/{actionId}/intents/{intentIndex}/steps/{stepIndex}` endpoint.
You have successfully orchestrated a cross-chain action without requiring the user to switch networks or manage gas on the destination chain.
# How It Works
Source: https://spiceflow-docs.spicenet.io/how-it-works
Understanding the technical flow behind Spice Flow API
## Naming Convention
The API uses the following naming convention:
* **Intent**: The parent action which contains ChainBatch(es)
* **ChainBatch**: For a specific chain, and has many Call(s)
* **Call**: Has `to`, `data`, and `value` parameters
## Process Flow
For a detailed technical explanation of the underlying technologies, including EIP-7702 and the `Delegate` contract, see our deep dive on [EIP-7702](/deep-dive/eip7702) and [Delegate Contract](/deep-dive/delegate).
## What Actually Happens
### 1. Action Submission
When you call `POST /actions`:
1. **Validation**: the API validates the action, its intents, and the `chainAuthorizations`
2. **Storage**: the action and its intents are persisted and assigned a tracking `actionId`
3. **Solver hand-off**: the intents are made available to the solver network
4. **Response**: returns the `actionId` for tracking
### 2. Step Execution
Each step runs at `POST /actions/{actionId}/intents/{intentIndex}/steps/{stepIndex}`:
1. **Step lookup**: retrieves the step by action id, intent index, and step index
2. **Execution**: performs the work for that step (checks, transactions)
3. **Result capture**: records results and any transaction hashes
4. **Status**: track progress with `GET /actions/{actionId}/intents/{intentIndex}/steps/{stepIndex}`
## Technical Implementation
### Intent Structure
```typescript theme={null}
interface Intent {
intentId: string; // Signature of chainBatches
chainBatches: ChainBatch[];
status: "created" | "executing" | "success" | "error" | "reverted";
steps: IntentStep[];
}
```
### Step Execution Model
```typescript theme={null}
interface IntentStep {
stepId: number; // Index [0, n) in chainBatches array
chainId: number; // Target chain for this step
status: "created" | "executing" | "success" | "error" | "reverted";
transactionHash?: string; // Hash if transaction was sent
executionResult?: object; // Results of step execution
}
```
### Transaction Submit Request Structure
```typescript theme={null}
interface TransactionSubmitRequest {
authorization: [
{
chainId: number;
address: string;
nonce: number;
r: string; // ECDSA signature R value
s: string; // ECDSA signature S value
yParity: number; // ECDSA signature Y parity
},
];
intentAuthorization: {
signature: string; // This becomes the intent ID
chainBatches: ChainBatch[];
};
}
```
## Key Concepts
### Intent ID Generation
* The intent ID is the **signature of the chainBatches**
* This approach is similar to Solana's transaction ID system
* Provides a unique, deterministic identifier for each intent
### Step Index System
* Steps are indexed as integers: `[0, n)` where n = chainBatches.length
* Step 0 = first chain authorization, Step 1 = second, etc.
* Steps must be executed sequentially in order
### Authorization Array
* **Multiple Authorizations**: Can submit multiple EIP-7702 authorizations in a single transaction
* **Cross-Chain Support**: Each authorization can be for different chains or addresses
* **Flexible Delegation**: Supports complex multi-chain execution patterns
### Execution Model
* **Active Execution**: Calling the POST endpoint triggers actual work
* **Unit of Work**: Each step call performs one discrete operation
* **Stateful**: Execution state is persisted in the database
* **Resumable**: Failed or interrupted intents can be resumed
## Error Handling
The API implements comprehensive error handling:
* **400 Bad Request**: Invalid transaction format, malformed parameters, or out-of-range step IDs
* **404 Not Found**: Intent not found or step index exceeds available steps
* **500 Internal Server Error**: Database errors, chain communication failures, or execution errors
```typescript theme={null}
// Example error response
{
"success": false,
"error": {
"message": "Step execution failed: insufficient gas"
}
}
```
## Data Persistence
Unlike the current in-memory implementation:
* **Database Storage**: All intent data is persisted to database
* **Reliable Execution**: Steps can be retried if they fail
* **Audit Trail**: Complete execution history is maintained
* **Recovery**: System can recover from restarts without losing state
## Monitoring
Comprehensive monitoring capabilities:
* **Database Logging**: All operations are logged to database
* **Execution Tracking**: Detailed step-by-step execution results
* **Transaction Hashes**: All on-chain transactions are tracked
* **Status Endpoints**: Real-time status via API calls
## Use Cases
### Perp DEXes
Power bridge-free native deposits from every network into margin accounts on Perp DEXes.
### Money Markets
Enable users to natively deposit assets directly into lending markets from every major network.
### AMMs
Facilitate bridge-free provision of liquidity from every network by enabling users to deposit assets into liquidity pools from any chain.
# Spice Flow
Source: https://spiceflow-docs.spicenet.io/index
A single SDK that lets your application work across every major EVM chain. Users deposit with any asset on any chain and your app executes on the destination chain instantly.
If you wish to integrate, please message our CEO [here](https://t.me/PeprikaInferno)
## Quick Start
### Installation
```bash theme={null}
npm install @spicenet-io/spiceflow-ui
```
### Basic Setup
Mount the provider stack (wagmi, react-query, Privy, then `SpiceFlowProvider` — see
[Quick Start](/quickstart#step-2-setup-providers)) and add `SpiceDeposit` to let users deposit
assets from any chain:
```tsx theme={null}
import { SpiceDeposit } from "@spicenet-io/spiceflow-ui";
// Rendered inside the provider stack
function DepositButton() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
setIsOpen(false)}
depositBatches={yourBatches}
styles={{ primaryColor: "#f97316" }}
/>
>
);
}
```
## Core Components
Root provider that manages wallet connections, chains, and branding
Full deposit flow with token selection, wallet connection, and execution
Withdraw assets from Spice balance back to wallet
Account overview popover with balance breakdown
## SDK Documentation
Get started with installation and configuration
Complete component reference and props
React hooks for building custom UIs
Customize components to match your brand
## Examples and Guides
Build your first Spice Flow app in 5 minutes
Complete code walkthrough for cross chain swaps
Security guidelines and integration patterns
Configure chains, providers, and execution modes
## Technical Deep Dive
Understand the architecture behind Spice Flow
Learn about EIP 7702 delegation
REST API documentation for advanced use cases
Deep dive into the on chain delegation contract
## Key Features
Make your application feel native to users across every major EVM compatible blockchain ecosystem.
Access users and volume from every major blockchain ecosystem without the need to redeploy smart contracts.
Users deposit with existing wallets and assets on their preferred networks. No bridging, no gas management.
Client side integration only. No smart contract changes needed. Drop in components and you're live.
## Need Help?
Get help from our community and team on Discord.
Connect with our CEO to get your questions answered and request a personalized, white glove onboarding to Spice Flow.
# Quick Start
Source: https://spiceflow-docs.spicenet.io/quickstart
Build your first Spice Flow app in 5 minutes
## Overview
This guide walks you through adding Spice Flow to your app. By the end, your users will be able to deposit assets from any chain into your protocol without bridging.
**What you'll build:**
* A wallet connection flow (embedded + external wallet)
* A deposit interface that accepts any token from any supported chain
* Cross chain execution powered by EIP 7702
***
## Step 1: Installation
Install the Spice Flow UI SDK and Privy for wallet management:
```bash npm theme={null}
npm install @spicenet-io/spiceflow-ui @spicenet-io/spiceflow-core @privy-io/react-auth react react-dom viem wagmi @tanstack/react-query
```
```bash yarn theme={null}
yarn add @spicenet-io/spiceflow-ui @spicenet-io/spiceflow-core @privy-io/react-auth react react-dom viem wagmi @tanstack/react-query
```
```bash pnpm theme={null}
pnpm add @spicenet-io/spiceflow-ui @spicenet-io/spiceflow-core @privy-io/react-auth react react-dom viem wagmi @tanstack/react-query
```
***
## Step 2: Setup Providers
`SpiceFlowProvider` reads wallet state from Privy, wagmi, and react-query — your app mounts all
four providers, in this order:
```tsx theme={null}
"use client"; // For Next.js App Router
import "@spicenet-io/spiceflow-ui/styles.css"; // required once, at app entry
import type { ReactNode } from "react";
import { WagmiProvider, createConfig, http } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PrivyProvider } from "@privy-io/react-auth";
import { SpiceFlowProvider } from "@spicenet-io/spiceflow-ui";
import { getMainnetChains } from "@spicenet-io/spiceflow-core";
import { defineChain, type Chain } from "viem";
// Every chain the installed SDK supports; new chains arrive with SDK upgrades.
const chains = getMainnetChains().map((c): Chain =>
defineChain({
id: c.id,
name: c.displayName,
nativeCurrency: c.nativeCurrency,
rpcUrls: { default: { http: [c.rpcUrl] } },
blockExplorers: { default: { name: "Explorer", url: c.blockExplorer } },
}),
) as [Chain, ...Chain[]];
const wagmiConfig = createConfig({
chains,
transports: Object.fromEntries(chains.map((c) => [c.id, http()])),
});
const queryClient = new QueryClient();
function App({ children }: { children: ReactNode }) {
return (
{/* supportedChainIds omitted: SpiceFlowProvider defaults to every supported chain */}
{children}
);
}
export default App;
```
Get your Privy App ID from [privy.io](https://privy.io) — it goes on `PrivyProvider`. This setup
enables every chain the SDK supports; to restrict, pass explicit chains to wagmi, Privy, and
`supportedChainIds`, keeping the three in sync. See [Configuration](/sdk/configuration).
***
## Step 3: Add Deposit Flow
Import `SpiceDeposit` and provide the on chain calls you want executed on the destination chain:
```tsx theme={null}
import { useState } from "react";
import { SpiceDeposit } from "@spicenet-io/spiceflow-ui";
// Rendered inside the providers from Step 2
function DepositButton() {
const [isOpen, setIsOpen] = useState(false);
// These are the calls that run on the destination chain after the user deposits
const depositBatches = [
{
chainId: 8453, // Base
calls: [
{ to: TOKEN_ADDRESS, value: 0n, data: encodeApprove(VAULT, amount) },
{ to: VAULT_ADDRESS, value: 0n, data: encodeDeposit(TOKEN_ADDRESS, amount) },
],
},
];
return (
<>
setIsOpen(false)}
depositBatches={depositBatches}
styles={{ primaryColor: "#f97316" }}
/>
>
);
}
```
That's it. The SDK handles the entire flow: Privy login, external wallet connection, token selection, escrow deposit, EIP 7702 delegation signing, and solver execution on the destination chain.
***
## Step 4: Add Withdrawals (Optional)
Let users withdraw their Spice balance back to their wallet:
```tsx theme={null}
import { SpiceWithdraw } from "@spicenet-io/spiceflow-ui";
setWithdrawOpen(false)}
styles={{ primaryColor: "#f97316" }}
/>
```
***
## Complete Example
Here's a complete Next.js page with deposit and withdraw:
```tsx theme={null}
"use client";
import { useState } from "react";
import { SpiceDeposit, SpiceWithdraw } from "@spicenet-io/spiceflow-ui";
// Rendered inside the providers from Step 2
function DeFiApp() {
const [depositOpen, setDepositOpen] = useState(false);
const [withdrawOpen, setWithdrawOpen] = useState(false);
return (
<>
setDepositOpen(false)}
depositBatches={[
{
chainId: 8453,
calls: [
// Your destination chain calls here
],
},
]}
styles={{ primaryColor: "#f97316" }}
onDepositSuccess={(detail) => {
console.log("Deposit succeeded:", detail);
}}
/>
setWithdrawOpen(false)}
styles={{ primaryColor: "#f97316" }}
/>
>
);
}
export default function Home() {
return ;
}
```
***
## Environment Setup
Create a `.env.local` file in your project root:
```env theme={null}
NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id_here
```
Get your credentials:
* **Privy:** Sign up at [privy.io](https://privy.io) and create an app
***
## How It Works
1. User opens your app and logs in via Privy (creates an embedded wallet)
2. User connects their external wallet (MetaMask, etc.)
3. User selects a token and amount to deposit
4. External wallet sends the token to the embedded wallet, then to the escrow
5. SDK signs an EIP 7702 delegation and submits it to the solver
6. Solver executes the `depositBatches` calls on the destination chain on behalf of the user
7. Success callback fires
The user never bridges. The user never pays gas on the destination chain.
***
## What's Next?
Explore all available components and their props
Build custom UIs with React hooks
Customize the appearance to match your brand
Configure chains, providers, and execution modes
***
## Troubleshooting
### "Window is not defined" Error
If you see this in Next.js, ensure you're using the `"use client"` directive:
```tsx theme={null}
"use client";
import { SpiceFlowProvider } from "@spicenet-io/spiceflow-ui";
```
### "usePrivy must be used within PrivyProvider" (or a wagmi context error)
The provider stack from Step 2 isn't mounted above the component. `SpiceFlowProvider` alone is not
enough — `WagmiProvider`, `QueryClientProvider`, and `PrivyProvider` must wrap it.
### Wallet Not Connecting
1. Check that your provider credentials are correct in `.env.local`
2. Ensure the provider is set to `"privy"`
3. Check the browser console for any error messages
### Need More Help?
Get help from the community and our team
# Components
Source: https://spiceflow-docs.spicenet.io/sdk/components
Complete reference for all Spice Flow SDK components
This reference is verified against the published `@spicenet-io/spiceflow-ui` package by
`tools/check-sdk-drift.mjs` on every change. If a component or prop below is wrong, that check fails.
## SpiceFlowProvider
The root provider for chain configuration, theming, and execution mode. It reads wallet state from
Privy, wagmi, and react-query — mount those above it in your app (see
[Quick Start Step 2](/quickstart#step-2-setup-providers)).
### Usage
```tsx theme={null}
import { SpiceFlowProvider } from "@spicenet-io/spiceflow-ui";
{children}
```
### Props
| Prop | Type | Required | Default | Description |
| ---------------------- | --------------------------------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider` | `"privy"` | No | `"privy"` | Wallet provider. Privy is the only provider in the current release |
| `children` | `ReactNode` | Yes | | Your application components |
| `network` | `"mainnet" \| "testnet"` | No | `"testnet"` | Network environment. **Set this explicitly**: leaving it default sends submissions to testnet |
| `nativeChainId` | `number` | No | | Chain where Spice Account deposits originate |
| `theme` | `SpiceTheme` | No | | Branding and theme config (see [Styling](/sdk/styling)) |
| `privyAppId` | `string` | No | | No effect — set `appId` on your own `PrivyProvider` |
| `supportedChainIds` | `number[]` | No | Per network | Override supported chain IDs |
| `allowedTokens` | `string[]` | No | | Restrict the token list by lowercase symbol, e.g. `["usdc", "eth"]` |
| `mode` | `"7702" \| "presign" \| "ondemand"` | No | `"7702"` | Execution mode |
| `whitelist` | `SpiceFlowWhitelistOptions \| false` | No | | Gate the flow to whitelisted wallets. When a user is not whitelisted the SDK forces `ondemand` mode and blanks the allowed tokens |
| `rpcOverrides` | `Record` | No | | Per-chain RPC URL overrides |
| `skipFlow` | `number[]` | No | `[]` | Chain IDs to skip in the flow |
| `appName` | `string` | No | `"Spicenet"` | Your application name |
| `apiUrl` | `string` | No | | Override the relayer API URL |
| `embeddedWalletConfig` | `{ createOnLogin?: "off" \| "users-without-wallets" \| "all-users" }` | No | | No effect — set `embeddedWallets` in the `PrivyProvider` config |
| `swapper` | `{ enabled?: boolean; integratorId?: string }` | No | off | Card and exchange deposit sources in `SpiceDeposit`, opt-in. See [Card and Exchange Deposits](/sdk/configuration#card-and-exchange-deposits) |
### Examples
```tsx Privy (Mainnet) theme={null}
{children}
```
```tsx Ondemand Mode theme={null}
{children}
```
***
## SpiceDeposit
The main deposit flow. Handles everything: Privy login, external wallet connection, token selection, escrow deposit, 7702 signing, and solver execution. This is the primary component most apps will use.
### Usage
```tsx theme={null}
import { SpiceDeposit } from "@spicenet-io/spiceflow-ui";
setIsOpen(false)}
depositBatches={batches}
styles={{ primaryColor: "#f97316" }}
/>
```
### Props
| Prop | Type | Required | Default | Description |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `isOpen` | `boolean` | Yes | | Control modal visibility |
| `onClose` | `() => void` | Yes | | Close handler |
| `styles` | `CustomStyles` | No | | Custom styling |
| `depositBatches` | `ChainBatch[] \| ((ctx: PostDepositContext) => Promise) \| ((amount, token) => Promise)` | No | | Destination chain calls to execute after deposit |
| `onDepositAmountChange` | `(amount: string) => void` | No | | Fires when the user changes the deposit amount |
| `destinationChainId` | `number` | No | | Target chain for execution |
| `destinationTokenAddress` | `string` | No | | Target token on the destination chain |
| `onDepositExecute` | `(amount: string) => Promise` | No | | Callback for non-7702 mode (app handles execution) |
| `depositActionLabel` | `string` | No | | Custom label for the action button |
| `sponsorGas` | `boolean` | No | `false` | Backend sponsors gas |
| `skipTokenSelection` | `boolean` | No | `false` | Skip the token selection step |
| `skipRecovery` | `boolean` | No | `false` | Skip the stranded-funds recovery prompt |
| `escrowRecovery` | `EscrowRecovery` | No | | Recover funds stuck in escrow |
| `onDepositSuccess` | `(detail) => void` | No | | Success callback. `detail.type` is `"deposit"` for an escrow deposit, or `"wallet-funding"` when a card or exchange deposit landed in an external wallet |
| `onDepositError` | `(error) => void` | No | | Error callback |
To restrict which tokens appear, set `allowedTokens` on `SpiceFlowProvider` (lowercase symbols), not on
`SpiceDeposit`.
With `swapper={{ enabled: true }}` on `SpiceFlowProvider`, the modal opens with a Wallet / Card / Exchange
chooser so users can fund by debit card or from an exchange. Off by default. See
[Card and Exchange Deposits](/sdk/configuration#card-and-exchange-deposits) for where the funds land in each
wallet mode.
### depositBatches
The `depositBatches` prop defines what happens on the destination chain after the deposit. It can be a static array or an async function that returns batches based on the deposit amount:
```tsx theme={null}
// Static batches
const batches = [
{
chainId: 8453,
calls: [
{ to: TOKEN, value: 0n, data: encodeApprove(VAULT, amount) },
{ to: VAULT, value: 0n, data: encodeDeposit(TOKEN, amount) },
],
},
];
// Dynamic batches (computed from deposit amount)
const dynamicBatches = async (amount: string, tokenAddress: string) => {
const parsedAmount = parseUnits(amount, 6);
return [
{
chainId: 8453,
calls: [
{ to: tokenAddress, value: 0n, data: encodeApprove(VAULT, parsedAmount) },
{ to: VAULT, value: 0n, data: encodeDeposit(tokenAddress, parsedAmount) },
],
},
];
};
```
***
## SpiceSupply
Runs a deposit / supply / stake action end to end: token selection, funding, fee quoting, and gasless
execution, driven by a `buildActionCalls` callback that returns your destination-chain calls. This is
the highest-leverage component for a "fund then act" flow (live production integrations build their pay
and supply surfaces on it), and it is far less code than wiring `useSpiceExecution` by hand.
### Usage
```tsx theme={null}
import { SpiceSupply, type BuildActionCallsContext, type Call } from "@spicenet-io/spiceflow-ui";
const buildActionCalls = (amount: bigint, ctx: BuildActionCallsContext): Call[] => [
{ to: ctx.destinationToken, value: 0n, data: encodeApprove(PROTOCOL, amount) },
{ to: PROTOCOL, value: 0n, data: encodeSupply(ctx.destinationToken, amount, ctx.userAddress) },
];
setIsOpen(false)}
actionLabel="Supply"
destinationToken={{ chainId: 8453, address: TOKEN, symbol: "USDC", decimals: 6 }}
buildActionCalls={buildActionCalls}
styles={{ primaryColor: "#f97316" }}
/>
```
`buildActionCalls` takes `(amount, ctx)` in that order. `ctx` (`BuildActionCallsContext`) carries
`userAddress`, `chainId`, `destinationToken`, and the fee fields. Read the current prop and context
shapes from the installed package (`SpiceSupplyProps`, `BuildActionCallsContext`).
***
## SpiceWithdraw
Withdraw assets from the user's Spice balance back to their wallet.
### Usage
```tsx theme={null}
import { SpiceWithdraw } from "@spicenet-io/spiceflow-ui";
setIsOpen(false)}
styles={{ primaryColor: "#f97316" }}
/>
```
### Props
| Prop | Type | Required | Default | Description |
| -------------------- | -------------------------------------------- | -------- | ------- | ----------------------------------------------- |
| `isOpen` | `boolean` | Yes | | Control modal visibility |
| `onClose` | `() => void` | Yes | | Close handler |
| `styles` | `CustomStyles` | No | | Custom styling |
| `onWithdrawExecute` | `(amount: string) => Promise` | No | | Callback for non-7702 mode |
| `onWithdrawSuccess` | `(data: { token, amount, chainId }) => void` | No | | Success callback |
| `onWithdrawError` | `(error: string, detail?) => void` | No | | Error callback |
| `availableTokens` | `Record` | No | | Restrict withdrawable tokens, keyed by chain ID |
| `buildSpicenetBatch` | `(params) => ChainBatch \| null` | No | | Override the Spicenet-side delivery batch |
***
## SpiceLock
Lock tokens for a duration (veToken style), with cross-chain deposits into the lock. The amount and
duration inputs are driven by hooks you create with `useAssetInput` and `useLockDuration` and pass in,
so you can read the live selection for your own preview UI.
### Usage
```tsx theme={null}
import { SpiceLock } from "@spicenet-io/spiceflow-ui";
setIsOpen(false)}
lockBatches={lockBatches}
lockTokenSymbol="veTOKEN"
lockChainId={4114}
durationOptions={[
{ label: "1 Month", months: 1 },
{ label: "3 Months", months: 3 },
{ label: "6 Months", months: 6 },
{ label: "12 Months", months: 12 },
]}
styles={{ primaryColor: "#f97316" }}
/>
```
### Props
| Prop | Type | Required | Default | Description |
| ----------------------------------- | ---------------------------- | -------- | ------- | ---------------------------------------------- |
| `isOpen` | `boolean` | Yes | | Modal visibility |
| `onClose` | `() => void` | Yes | | Close handler |
| `lockBatches` | `ChainBatch[]` | Yes | | On-chain lock calls |
| `lockTokenSymbol` | `string` | Yes | | Display symbol for the lock token |
| `lockChainId` | `number` | Yes | | Chain where the lock contract lives |
| `durationOptions` | `LockDurationOption[]` | Yes | | Available lock durations |
| `styles` | `CustomStyles` | No | | Custom styling |
| `dark` | `boolean` | No | | Dark mode override |
| `lockTokenLogoURI` | `string` | No | | Logo URL for the lock token |
| `votingPowerEstimate` | `(amount, months) => number` | No | | Estimate voting power from amount and duration |
| `sourceToDestinationConversionRate` | `number` | No | | Conversion rate display |
| `onLockSuccess` | `(txHash?: string) => void` | No | | Success callback |
| `onAddFunds` | `() => void` | No | | Handler for "add funds" |
| `destinationTokenAddress` | `Address` | No | | Target token on the lock chain |
`SpiceLock` also accepts hoistable input hooks (`sourceAssetInputHook`, `lockDurationHook`) and
`escrowAddress`. See `SpiceLockModalProps` in the installed package for the full, current shape.
***
## AccountDisplay
A popover that shows the user's Spice balance breakdown, pending deposits, and quick actions for deposit/withdraw. Great for header navigation.
### Usage
```tsx theme={null}
import { AccountDisplay } from "@spicenet-io/spiceflow-ui";
setDepositOpen(true)}
onWithdrawClick={() => setWithdrawOpen(true)}
/>
```
### Props
| Prop | Type | Required | Default | Description |
| ------------------ | ------------------------ | -------- | ---------- | ----------------------------------------------------------- |
| `styles` | `CustomStyles` | No | | Custom styling |
| `dark` | `boolean` | No | | Dark mode override |
| `appName` | `string` | No | `"Spice"` | Display name |
| `status` | `"active" \| "inactive"` | No | `"active"` | Account status indicator |
| `statusLabel` | `string` | No | | Custom status text |
| `onDepositClick` | `() => void` | No | | Deposit button handler |
| `onWithdrawClick` | `() => void` | No | | Withdraw button handler |
| `onDepositSuccess` | `() => void` | No | | Called after a successful deposit |
| `customSections` | `CustomSection[]` | No | | Add custom sections to the popover |
| `extraAssets` | `SpiceAsset[]` | No | | Additional assets to display (e.g. the app's own positions) |
| `tokenPrices` | `Record` | No | | Token price overrides |
| `showModeToggle` | `boolean` | No | | Show the Embedded / External mode toggle |
| `defaultMode` | `"7702" \| "non7702"` | No | | Initial mode for the toggle |
***
## SelectChainModal
The source-chain picker on its own, for when you want the chain-selection step outside the full deposit
flow. See `SelectChainModal` in the installed package for its current props.
```tsx theme={null}
import { SelectChainModal } from "@spicenet-io/spiceflow-ui";
```
***
## ProviderLogin
Wallet connection button for the configured provider (Privy).
### Usage
```tsx theme={null}
import { ProviderLogin } from "@spicenet-io/spiceflow-ui";
console.log("Logged in")} />
```
### Props
| Prop | Type | Required | Description |
| --------------- | ------------ | -------- | ---------------------------------------- |
| `onAuthSuccess` | `() => void` | No | Callback after successful authentication |
| `autoTrigger` | `boolean` | No | Automatically trigger login on mount |
***
## Next Steps
Build custom UIs with React hooks
Customize component appearance
Configure chains and providers
See complete examples
# Configuration
Source: https://spiceflow-docs.spicenet.io/sdk/configuration
Configure wallet providers, supported chains, and SDK behavior
## Wallet Provider
Spice Flow uses Privy for wallet connections and embedded wallets.
### Privy
Best for embedded wallets and social logins.
```tsx theme={null}
{children}
```
**Configuration:** your Privy app ID and embedded-wallet policy are configured on your own
`PrivyProvider` (which the SDK reads through Privy's hooks), not on `SpiceFlowProvider`:
```tsx theme={null}
```
The `privyAppId` and `embeddedWalletConfig` props on `SpiceFlowProvider` exist for compatibility but
have no effect in the current release.
**Get Your Privy App ID:**
1. Sign up at [privy.io](https://privy.io)
2. Create a new app
3. Copy your App ID from the dashboard
***
## Execution Modes
Spice Flow supports three execution modes.
### 7702 Mode (Default)
Uses EIP 7702 with embedded wallets. The solver executes transactions on behalf of the user via delegate contracts. This is the default and recommended mode for most chains.
```tsx theme={null}
{children}
```
### Presign Mode
Users sign all transactions upfront using their external wallet. Used for chains that don't support EIP 7702.
```tsx theme={null}
{children}
```
### Ondemand Mode
Each transaction is signed on demand during execution. The app handles execution via `onDepositExecute` / `onWithdrawExecute` callbacks.
```tsx theme={null}
{children}
```
In `presign` and `ondemand` modes, the embedded wallet flow is skipped. Components use callback props (like `onDepositExecute`) instead of the solver execution path.
***
## Card and Exchange Deposits
Available from `@spicenet-io/spiceflow-ui@4.7.11`. When enabled, `SpiceDeposit` opens with a source
chooser in front of the existing flow: **Wallet**, **Card**, or **Exchange**. Card and exchange deposits
run through the Swapper Finance widget, and the SDK verifies on chain that the USDC arrived before handing
off. Users can fund with a debit card or from an exchange account instead of only from a connected wallet.
It is **off by default**. Nothing changes for your integration until you opt in:
```tsx theme={null}
{children}
```
`enabled: true` is the only required field. The integrator id defaults to Spicenet's; pass
`integratorId` only if Swapper has issued you your own.
### Where the funds land
Swapper delivers USDC to the wallet the user is operating with, and the wallet mode decides what happens
next.
| Wallet mode | Delivered to | Chain | Then |
| --------------------------------- | --------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Embedded (`7702`) | The user's embedded wallet | Arbitrum | Continues into the escrow deposit and credits the spice balance, which is usable on any supported chain |
| External (`presign` / `ondemand`) | The user's connected wallet | Your app's chain if it is Base or Arbitrum, otherwise Arbitrum | The flow ends. Funds are in the user's wallet and no spice balance is credited |
Swapper currently delivers to **Base** and **Arbitrum**. An embedded deposit always lands on Arbitrum
because escrow abstracts the chain downstream. An external deposit lands on your app's chain when Swapper
covers it, and on Arbitrum when it does not. The app's chain is read from `destinationChainId` on
`SpiceDeposit`, falling back to `nativeChainId` on the provider.
If your app runs on a chain Swapper does not deliver to, an external-wallet user's card or exchange deposit
ends up on Arbitrum, in their own wallet. Consider enabling this only for embedded-wallet users on those
chains, or make sure your flow can handle funds arriving there.
### Telling the two outcomes apart
`onDepositSuccess` on `SpiceDeposit` reports which path completed through `detail.type`:
```tsx theme={null}
{
if (detail.type === "wallet-funding") {
// Card/exchange deposit landed in the user's external wallet. No spice credited.
return;
}
// "deposit": escrow deposit completed and spice was credited.
}}
/>
```
Code that already checks for `"deposit"` keeps working and will not treat a wallet funding as a spice
credit.
The source chooser only appears on `network="mainnet"`; testnets skip it and go straight to the wallet
flow.
***
## Network Configuration
Set the network to control which chains are available:
```tsx theme={null}
```
### Default Chains by Network
As of `@spicenet-io/spiceflow-ui@4.7.9`:
**Mainnet:** Ethereum (1), Base (8453), Arbitrum (42161), Citrea (4114), Robinhood (4663), BSC (56), Polygon (137), Monad (143)
**Testnet:** Sepolia (11155111), Arbitrum Sepolia (421614), Base Sepolia (84532), Citrea Testnet (5115), Base Camp (123420001114), Pharos (688688), Pharos Atlantic (688689), Robinhood Testnet (46630), Monad Testnet (10143), BSC Testnet (97), Avalanche Fuji (43113), Polygon Amoy (80002), Optimism Sepolia (11155420)
The list grows with releases — read it from your installed version with `getChainIdsByNetwork("mainnet")` (exported from the SDK) rather than hardcoding.
### Custom Chain IDs
Override the defaults with `supportedChainIds`:
```tsx theme={null}
{children}
```
***
## Liquidity Venues
Available from `@spicenet-io/spiceflow-ui@4.8.0`. When a flow has to swap on the destination chain,
the SDK quotes every liquidity venue that covers that chain and takes the best fill: the most output
for an exact input, the least input for an exact output. Routing is decided in the SDK, on your RPC.
The solver only executes the calls the winning quote produced.
Leave `liquidityVenues` off and you get the built-in venues:
| Chain | Built-in venues |
| ----------------------------------------------------- | -------------------------------------------- |
| Citrea (4114, 5115) | Satsuma |
| Base (8453) | Uniswap v3, Uniswap v4, Aerodrome Slipstream |
| Robinhood (4663) | Uniswap v3, SushiSwap, Uniswap v4 |
| Ethereum, Arbitrum, Optimism, Polygon, Avalanche, BSC | None usable — see below |
Every built-in serves both swap directions, so a venue is never reachable for an exact-input swap
but missing for an exact-output one.
The SDK also registers a 1inch venue, but it carries no API key and 1inch rejects keyless requests,
so it never returns a quote, and there is no way to supply a key through this prop. On chains where
1inch is the only thing covering you — the last row above — **declare your own venue**, or a swap on
that chain has nothing to route through.
### Adding your own venue
Declare it once on the provider and every component picks it up. This is the whole change needed to
route through a deployment the SDK does not ship — here, the SushiSwap v3 deployment on Robinhood:
```tsx theme={null}
{children}
```
Your venue races the built-ins on price. It does not override them, and it is used in both swap
directions.
### Turning the built-ins off
Set `includeDefaults: false` to drop every built-in venue, including 1inch, and route only through
what you declared:
```tsx theme={null}
liquidityVenues={{
venues: [ /* ... */ ],
includeDefaults: false,
}}
```
On a chain with no venue at all, quoting then fails naming that chain rather than falling back to
something you did not configure. Leave `includeDefaults` off (or `true`) to keep the built-ins and
add yours alongside them.
### Venue fields
Supply the **deployment's** addresses, not a pool address. A v3-style pool calls back into
`msg.sender`, so swaps go through the router and quotes through the quoter; the SDK finds the pools
itself from the fee tiers (or tick spacings) and hub currencies.
| `protocol` | Required | Optional |
| ---------------------- | ------------------------------------------------- | -------------------------------------------------------- |
| `uniswap-v3` | `chainId`, `swapRouter`, `quoterV2` | `name`, `feeTiers`, `hubCurrencies`, `routerHasDeadline` |
| `uniswap-v4` | `chainId`, `quoter`, `universalRouter`, `permit2` | `name`, `hubCurrencies` |
| `aerodrome-slipstream` | `chainId`, `swapRouter`, `quoterV2` | `name`, `tickSpacings`, `hubCurrencies` |
Each entry names exactly one chain. Declare the same deployment twice if it runs on two chains.
* **`name`** is shown to the user as `via ` once an exact-output swap is quoted. It defaults to
the protocol.
* **`hubCurrencies`** are the intermediate tokens tried for two-hop routes, usually wrapped native and
the chain's main stablecoin.
* **`feeTiers`** / **`tickSpacings`** are the pool tiers to search. Include any non-standard tier your
venue uses, such as PancakeSwap's 2500.
Set `routerHasDeadline: true` when the deployment forked the **original** Uniswap `SwapRouter`, whose
exact-output structs carry a deadline. SwapRouter02 forks do not, and this is the default.
Getting it wrong is invisible until execution: quoting goes through the quoter, which does not care,
so the quote looks correct either way and only the transaction reverts. If your exact-output swaps
quote cleanly but revert on chain, this flag is the first thing to check.
Every venue covering a chain is quoted on every keystroke, and each one costs several quoter calls
against **your** RPC (roughly `feeTiers x (1 + hubCurrencies)` per venue). Adding venues improves
fill and increases RPC load. Scope `chainId` tightly and keep `hubCurrencies` to the tokens that
actually carry liquidity.
### Venues from another protocol family
Most DEXs are forks of one of the three families above, so adding one is addresses only. A venue
built on a different family — a Uniswap v2 pair, Curve, Balancer — needs an adapter inside the SDK,
because the SDK owns the calldata rather than asking you to encode swaps yourself. Send us the chain,
router, quoter and one funded pool and it becomes a `protocol` every integration can name.
***
## Theming
Set your app's visual identity through the `theme` prop. All SDK components inherit these values.
```tsx theme={null}
{children}
```
See [Styling](/sdk/styling) for the full theming reference.
***
## API URL Override
By default, the SDK connects to the Spicenet relayer. Override this if you're proxying requests or using a custom deployment:
```tsx theme={null}
{children}
```
***
## Complete Provider Reference
| Prop | Type | Default | Description |
| ---------------------- | ------------------------------------ | ------------ | -------------------------------------------------------------------------------------------------- |
| `provider` | `"privy"` | `"privy"` | Wallet provider (Privy only in the current release) |
| `network` | `"mainnet" \| "testnet"` | `"testnet"` | Network environment |
| `nativeChainId` | `number` | | User's default chain |
| `theme` | `SpiceTheme` | | Colors, dark mode, surfaces, fonts, app name |
| `privyAppId` | `string` | | No effect — set `appId` on `PrivyProvider` |
| `supportedChainIds` | `number[]` | Per network | Override supported chains |
| `allowedTokens` | `string[]` | | Restrict tokens by lowercase symbol |
| `mode` | `"7702" \| "presign" \| "ondemand"` | `"7702"` | Execution mode |
| `whitelist` | `SpiceFlowWhitelistOptions \| false` | | Gate the flow to whitelisted wallets |
| `rpcOverrides` | `Record` | | Per-chain RPC overrides |
| `skipFlow` | `number[]` | `[]` | Chain IDs to skip in the flow |
| `appName` | `string` | `"Spicenet"` | Application name |
| `apiUrl` | `string` | | Override relayer API URL |
| `embeddedWalletConfig` | `object` | | No effect — set `embeddedWallets` in the `PrivyProvider` config |
| `swapper` | `SpiceSwapperConfig` | off | Enable card and exchange deposits. See [Card and Exchange Deposits](#card-and-exchange-deposits) |
| `liquidityVenues` | `LiquidityVenuesConfig` | Built-ins | Declare your own swap venues, or turn the built-ins off. See [Liquidity Venues](#liquidity-venues) |
***
## Next Steps
Customize component appearance
Explore available components
See complete examples
Security and integration patterns
# Hooks
Source: https://spiceflow-docs.spicenet.io/sdk/hooks
React hooks for building custom UIs with Spice Flow
These hooks let you build custom UIs on top of the Spice Flow SDK. If the prebuilt components don't fit your use case, use these hooks to access wallet state, balances, and execution directly.
## useSpiceExecution
The core execution hook. Handles EIP 7702 delegation signing, intent submission, and solver execution polling. This is what powers `SpiceDeposit` under the hood.
### Usage
```tsx theme={null}
import { useSpiceExecution } from "@spicenet-io/spiceflow-ui";
function CustomDeposit() {
const { execute, estimate, clearStatus } = useSpiceExecution();
const handleExecute = async () => {
const actionId = await execute(
chainBatches, // destination chain calls
tokenAddress, // token being deposited
tokenTransferAmount, // amount in wei
(progress) => { // progress callback
console.log(progress.step, progress.message);
},
);
console.log("Action completed:", actionId);
};
return ;
}
```
### Return Value
The hook returns `execute` (not `executeGasless`), plus fee estimation, status, and a status reset:
```typescript theme={null}
{
execute: (
chainBatches: ChainBatch[],
tokenAddress: string,
tokenTransferAmount: bigint,
onProgress?: (progress: ExecutionProgress) => void,
signal?: AbortSignal,
options?: ExecutionOptions,
) => Promise; // returns actionId
estimate: (chainBatches: ChainBatch[], tokenAddress: string, amount: bigint) => Promise;
lastFeeEstimate: FeeEstimate | null;
hasEmbeddedWallet: boolean;
intentStatus: unknown;
clearStatus: () => void; // call between sequential executions (there is no reset())
}
```
The SDK holds its own concurrency lock and throws on an overlapping `execute` call, so you do not need
a module-level mutex. For multi-step flows, call `clearStatus()` between executions.
### Progress Steps
The `onProgress` callback receives updates as execution proceeds:
| Step | Description |
| -------------------- | -------------------------------------------- |
| `idle` | Not started |
| `building` | Building the intent |
| `signing-delegation` | User signing EIP 7702 delegation |
| `signing-intent` | User signing the intent hash |
| `submitting` | Submitting to the solver |
| `executing` | Solver executing on chain |
| `confirming-rollup` | Confirming settlement on the Spicenet rollup |
| `success` | Execution complete |
| `error` | Something went wrong |
***
## useSpiceAssets
Fetch the user's Spice balance from the relayer API. This returns the off chain balance (what the user has deposited via escrow), not on chain wallet balances.
### Usage
```tsx theme={null}
import { useSpiceAssets } from "@spicenet-io/spiceflow-ui";
function BalanceDisplay() {
const { assets, loading, hasBalance, refetch, getAssetsByChain } = useSpiceAssets({
address: "0x...",
supportedChains: [8453, 4114],
refetchInterval: 30000,
});
if (loading) return
Loading...
;
if (!hasBalance) return
No deposits yet
;
return (
{assets.map(asset => (
{asset.symbol}: {asset.balanceFormatted} on chain {asset.chainId}
))}
);
}
```
### Config
| Param | Type | Default | Description |
| ----------------- | ---------- | ------- | --------------------------- |
| `address` | `string` | | Wallet address to query |
| `supportedChains` | `number[]` | | Filter to specific chains |
| `enabled` | `boolean` | `true` | Enable or disable fetching |
| `refetchInterval` | `number` | | Auto refetch interval in ms |
### Return Value
| Field | Type | Description |
| ------------------ | ---------------------- | --------------------------------------- |
| `assets` | `Asset[]` | Array of deposited assets with balances |
| `loading` | `boolean` | True during initial load |
| `error` | `string \| null` | Error message if fetch failed |
| `hasBalance` | `boolean` | Whether user has any balance |
| `refetch` | `() => Promise` | Manually refetch balances |
| `getAssetsByChain` | `(chainId) => Asset[]` | Filter assets by chain |
***
## useSupplyAssets
Lists every asset the user can fund a given destination token with — wallet balances and Spice
balances, across all supported chains. This is the source-selection half of a custom supply UI (see
the [Custom UI Integration](/guides/custom-ui) guide for the full flow).
### Usage
```tsx theme={null}
import { useSupplyAssets } from "@spicenet-io/spiceflow-ui";
const { displayAssets, embeddedWalletAddress, externalAddress, isNon7702 } = useSupplyAssets({
address, // user wallet address
destinationToken: { chainId: 8453, address: USDC, symbol: "USDC", decimals: 6 },
sourceChains: [1, 8453, 42161], // optional: restrict source chains
supportedSourceAssets: ["usdc", "eth"], // optional: restrict by symbol
enabled: true,
});
```
### Return Value
| Field | Type | Description |
| ----------------------- | ---------------------------------------------- | ------------------------------------------------- |
| `displayAssets` | `(Asset & { _source: "wallet" \| "spice" })[]` | Fundable assets across chains, tagged by source |
| `filteredSpiceAssets` | `Asset[]` | Spice balance assets only |
| `walletAssets` | `Asset[]` | Wallet balance assets only |
| `embeddedWalletAddress` | `` `0x${string}` \| undefined `` | The user's embedded wallet |
| `externalAddress` | `` `0x${string}` \| undefined `` | The user's external wallet |
| `isNon7702` | `boolean` | True when running without an embedded 7702 wallet |
| `refreshSpiceAssets` | `() => Promise` | Refetch Spice balances |
| `refreshWalletAssets` | `() => void` | Refetch wallet balances |
***
## useSupplyQuote
Resolves routing for a selected source asset: whether it funds the destination token directly or
needs a swap, and the quote for it. Feed its output into `buildSupplyChainBatches` /
`estimateFeePreview` — see [Custom UI Integration](/guides/custom-ui).
### Usage
```tsx theme={null}
import { useSupplyQuote } from "@spicenet-io/spiceflow-ui";
const quote = useSupplyQuote({
direction: "input", // amount is what the user pays ("output": what the market receives)
selectedAsset, // { asset, amount } or null
destinationToken,
chainId: destinationToken.chainId,
recipient: embeddedWalletAddress,
paymentAmount: amount,
});
```
### Return Value
| Field | Type | Description |
| ---------------------- | --------------------------------- | ------------------------------------------------------------------ |
| `isQuoting` | `boolean` | Quote in flight |
| `quoteError` | `string \| null` | Quote failure |
| `estimatedOutput` | `string \| null` | Destination amount for `direction: "input"` |
| `estimatedInput` | `string \| null` | Source amount for `direction: "output"` |
| `needsSwap` | `boolean` | Source asset must be swapped into the destination token |
| `isDirect` | `boolean` | Direct funding, no swap |
| `resolvedSwap` | `ResolvedSwap \| null` | The resolved routing swap |
| `exactOutputSwap` | `ResolvedExactOutputSwap \| null` | Exact-output variant when quoting by output |
| `quoteAmountOut` | `bigint \| null` | Raw quoted output amount |
| `resolvedTokenIn` | `Address \| null` | The destination-chain token the funding arrives in |
| `selectedIsEquivalent` | `boolean` | Source is the same asset as the destination token on another chain |
***
## useWallet
Access wallet connection state and signing actions for the configured provider (Privy).
### Usage
```tsx theme={null}
import { useWallet } from "@spicenet-io/spiceflow-ui";
function WalletInfo() {
const { address, isConnected, isAuthenticated, provider, actions } = useWallet();
if (!isConnected) return
Not connected
;
return (
Connected via {provider}: {address}
);
}
```
### Return Value
| Field | Type | Description |
| --------------------------- | ------------------------------------ | --------------------------- |
| `isReady` | `boolean` | Provider is initialized |
| `isAuthenticated` | `boolean` | User has authenticated |
| `isConnected` | `boolean` | Wallet is connected |
| `address` | `Address \| undefined` | Connected wallet address |
| `provider` | `"privy" \| null` | Active provider |
| `actions.signMessage` | `(message) => Promise<{signature}>` | Sign a message |
| `actions.signAuthorization` | `(params) => Promise` | Sign EIP 7702 authorization |
***
## Getting the embedded wallet address
There is no standalone `useEmbeddedWalletAddress` hook. In 7702 mode the embedded wallet is the
on-chain identity; read its address from `useWallet()` or from the provider context:
```tsx theme={null}
import { useContext } from "react";
import { useWallet, SpiceFlowProviderContext } from "@spicenet-io/spiceflow-ui";
function EmbeddedWallet() {
const ctx = useContext(SpiceFlowProviderContext);
const embeddedAddress = ctx?.embeddedWalletAddress;
if (!embeddedAddress) return
No embedded wallet
;
return
Embedded: {embeddedAddress}
;
}
```
***
## useAssetInput
Manage asset selection and amount input state. Used to control the asset selector in deposit and lock flows from your own UI.
### Usage
```tsx theme={null}
import { useAssetInput } from "@spicenet-io/spiceflow-ui";
function CustomInput() {
const { selectedAsset, setSelectedAsset, assetAmount, setAssetAmount } = useAssetInput();
return (
setAssetAmount(e.target.value)}
placeholder="Amount"
/>
);
}
```
### Return Value
| Field | Type | Description |
| ------------------ | ----------------------- | ------------------------ |
| `selectedAsset` | `SelectedAsset \| null` | Currently selected asset |
| `setSelectedAsset` | `(asset) => void` | Set the selected asset |
| `assetAmount` | `string` | Current amount string |
| `setAssetAmount` | `(amount) => void` | Set the amount |
***
## useLockDuration
Manage lock duration selection state for `SpiceLock`.
### Usage
```tsx theme={null}
import { useLockDuration } from "@spicenet-io/spiceflow-ui";
const durationOptions = [
{ label: "1 Month", months: 1 },
{ label: "6 Months", months: 6 },
{ label: "12 Months", months: 12 },
];
function DurationPicker() {
const { selectedDurationIdx, setSelectedDurationIdx, selectedDuration } = useLockDuration(durationOptions);
return (
{durationOptions.map((opt, i) => (
))}
);
}
```
***
## useSpicePendingDeposits
Detect funds stranded in the user's embedded wallet (deposits that were sent but not yet credited to Spice balance). Used by SpiceDeposit internally for recovery flows.
### Usage
```tsx theme={null}
import { useSpicePendingDeposits } from "@spicenet-io/spiceflow-ui";
function PendingCheck() {
const { pendingDeposits } = useSpicePendingDeposits({
chainIds: [8453, 4114],
});
if (pendingDeposits.length > 0) {
return
You have {pendingDeposits.length} pending deposit(s) to recover.
;
}
return null;
}
```
***
## useSpiceBrand
Resolve the current theme from the provider's `theme` prop and any component-level `styles`/`dark` overrides. Only used when building custom components that need to match the SDK's look.
### Usage
```tsx theme={null}
import { useSpiceBrand } from "@spicenet-io/spiceflow-ui";
function CustomCard({ styles, dark }) {
const { primaryColor, dk, palette } = useSpiceBrand(styles, dark);
return (
Themed card
);
}
```
### Return Value
| Field | Type | Description |
| -------------- | --------------------- | ------------------------------------- |
| `dark` | `boolean` | Whether dark mode is active |
| `primaryColor` | `string` | Brand accent color |
| `theme` | `Theme` | Full theme object |
| `dk` | `ResolvedDarkPalette` | Dark palette values |
| `palette` | `ResolvedDarkPalette` | Mode resolved palette (light or dark) |
***
## Next Steps
Use prebuilt components for common flows
Customize appearance with the brand system
Explore the REST API for advanced use cases
Configure chains and providers
# Installation & Setup
Source: https://spiceflow-docs.spicenet.io/sdk/installation
Get started with the Spice Flow SDK in your React application
## Installation
Install the Spice Flow SDK using npm, yarn, or pnpm:
```bash theme={null}
npm install @spicenet-io/spiceflow-ui
```
```bash theme={null}
yarn add @spicenet-io/spiceflow-ui
```
```bash theme={null}
pnpm add @spicenet-io/spiceflow-ui
```
## Peer Dependencies
The SDK requires the following peer dependencies. Install based on your wallet provider:
### Required Dependencies
These are always required:
```bash theme={null}
npm install react react-dom viem wagmi @tanstack/react-query
```
| Package | Version | Description |
| ----------------------- | ---------- | -------------------------- |
| `react` | `^18.0.0` | React library |
| `react-dom` | `^18.0.0` | React DOM |
| `viem` | `>=2.21.0` | Ethereum interface library |
| `wagmi` | `>=2.12.0` | React hooks for Ethereum |
| `@tanstack/react-query` | `>=5.0.0` | Data fetching library |
A peer-dependency conflict on a fresh install is expected (the SDK declares React 18 while some peers
accept 18 or 19). Use `--legacy-peer-deps` and keep the app on React 18.
### Wallet Provider
The SDK uses Privy for wallet connections:
```bash theme={null}
npm install @privy-io/react-auth
```
| Package | Version | Provider |
| ---------------------- | ----------------- | -------- |
| `@privy-io/react-auth` | `>=2.24.0 <4.0.0` | Privy |
***
## Complete Installation
```bash theme={null}
npm install @spicenet-io/spiceflow-ui @spicenet-io/spiceflow-core @privy-io/react-auth react react-dom viem wagmi @tanstack/react-query
```
***
## Basic Setup
### 1. Mount the Provider Stack
`SpiceFlowProvider` reads wallet state from Privy, wagmi, and react-query — it does not mount them.
Your app provides all four, in this order, and imports the SDK stylesheet once at app entry:
```tsx theme={null}
import "@spicenet-io/spiceflow-ui/styles.css";
{/* Your app components */}
```
See [Quick Start Step 2](/quickstart#step-2-setup-providers) for the full wagmi and Privy configs.
Already running Privy? Reuse your existing `PrivyProvider` — add the chains you enable to its
`supportedChains` and your wagmi config.
### 2. Add Wallet Connection
Use the `ProviderLogin` component to let users connect their wallet:
```tsx theme={null}
import { ProviderLogin } from "@spicenet-io/spiceflow-ui";
function Header() {
return (
);
}
```
### 3. Add a Deposit Flow
Add `SpiceDeposit` to let users deposit from any chain:
```tsx theme={null}
import { useState } from "react";
import { SpiceDeposit } from "@spicenet-io/spiceflow-ui";
function DepositPage() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
setIsOpen(false)}
depositBatches={[
{
chainId: 8453,
calls: [
// Your destination chain calls here
],
},
]}
styles={{ primaryColor: "#f97316" }}
/>
>
);
}
```
***
## Complete Example
Here's a complete Next.js example with deposit and withdraw:
```tsx theme={null}
"use client";
import { useState } from "react";
import { SpiceDeposit, SpiceWithdraw } from "@spicenet-io/spiceflow-ui";
// Rendered inside the provider stack from Basic Setup
function DeFiApp() {
const [depositOpen, setDepositOpen] = useState(false);
const [withdrawOpen, setWithdrawOpen] = useState(false);
return (
<>
setDepositOpen(false)}
depositBatches={[]}
styles={{ primaryColor: "#f97316" }}
/>
setWithdrawOpen(false)}
styles={{ primaryColor: "#f97316" }}
/>
>
);
}
export default function Home() {
return ;
}
```
***
## Next.js Considerations
If you're using Next.js with Server-Side Rendering (SSR), wrap components that use Spice Flow in a client component:
```tsx theme={null}
"use client"; // Add this directive at the top
import { SpiceFlowProvider } from "@spicenet-io/spiceflow-ui";
```
Or check for the window object:
```tsx theme={null}
if (typeof window === 'undefined') {
return
Loading...
;
}
```
***
## Environment Variables
Store your provider credentials in environment variables:
```env theme={null}
NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id
```
Then use them in your code:
```tsx theme={null}
```
***
## Next Steps
Learn about all available components and their props
Use React hooks to build custom UIs
Configure wallet providers, chains, and more
Build your first Spice Flow app step-by-step
# Styling and Theming
Source: https://spiceflow-docs.spicenet.io/sdk/styling
Customize the appearance of Spice Flow components to match your brand
## Theme System
Set your brand once on `SpiceFlowProvider`. Every SDK component inherits it automatically.
```tsx theme={null}
{children}
```
### SpiceTheme Options
| Option | Type | Default | Description |
| -------------- | ----------- | ------------------ | ------------------------------------------------- |
| `primaryColor` | `string` | `"#EA4B4B"` | Accent color — buttons, highlights, active states |
| `dark` | `boolean` | `false` | Dark or light mode |
| `shell` | `string` | Mode default | Modal outer background |
| `card` | `string` | Mode default | Card and input area background |
| `text` | `string` | Mode default | Primary text color |
| `textMuted` | `string` | Mode default | Secondary/label text color |
| `border` | `string` | Mode default | Border color |
| `borderRadius` | `string` | `"8px"` | Border radius applied across all components |
| `fontFamily` | `string` | `"Helvetica Neue"` | Primary font family |
| `appName` | `string` | `"Spicenet"` | App name shown in modal headers |
| `logo` | `ReactNode` | | Custom logo for modal headers |
Surface tokens (`shell`, `card`, `text`, `textMuted`, `border`) have sensible defaults per mode — only set what you want to override.
***
## Dark Mode
```tsx theme={null}
// Dark
// Light
```
### Default Surface Colors
| Token | Dark | Light |
| ----------- | ------------------ | ------------------ |
| `shell` | `#141414` | `#ffffff` |
| `card` | `#1e1e1e` | `#f9fafb` |
| `text` | `#ffffff` | `#111827` |
| `textMuted` | `#888888` | `#6b7280` |
| `border` | `{primaryColor}33` | `{primaryColor}22` |
***
## Custom Surfaces
Override any surface token alongside the mode:
```tsx theme={null}
```
***
## Per-Component Override
Pass `styles` to any component to override the provider theme for that instance only:
```tsx theme={null}
// Different accent on a single modal
// Force dark on one component even if provider is light
// Full button + input customisation
```
### Available `styles` keys per component
| Component | Keys |
| ---------------- | ------------------------------------------------------------------------------------------------------------- |
| `SpiceDeposit` | `primaryColor`, `button.backgroundColor`, `button.color`, `button.borderRadius`, `inputVariant`, `fontFamily` |
| `SpiceWithdraw` | `primaryColor`, `button.backgroundColor`, `button.color`, `fontFamily` |
| `SpiceSupply` | `primaryColor`, `button.backgroundColor`, `button.color`, `inputVariant` |
| `SpiceLock` | `primaryColor`, `button.backgroundColor`, `button.color`, `fontFamily` |
| `AccountDisplay` | `primaryColor`, `fontFamily` |
`inputVariant` accepts `"light"` or `"dark"` to override the input field style independently of the global mode.
***
## Fonts
The SDK uses two fonts:
* **Helvetica Neue** — all body text, labels, headings, descriptions
* **IBM Plex Mono** — amounts, addresses, hashes, button labels
Override the body font via `theme.fontFamily`. The monospace font is always used for technical displays.
***
## Examples
### Orange Dark
```tsx theme={null}
```
### Blue Light
```tsx theme={null}
```
### Green Dark with Custom Surfaces
```tsx theme={null}
```
### Sharp Trading UI
```tsx theme={null}
```
***
## Building Custom Components
Use `useSpiceBrand` to match the SDK theme inside your own components:
```tsx theme={null}
import { useSpiceBrand } from "@spicenet-io/spiceflow-ui";
function CustomCard() {
const { primaryColor, dark, palette } = useSpiceBrand();
return (
Custom card
Matches SDK palette
);
}
```
`palette` is the fully-resolved palette for the current mode — dark or light values depending on `dark`. It exposes: `shell`, `cardBg`, `inputBg`, `hoverBg`, `textPrimary`, `textSecondary`, `inputText`, `inputPlaceholder`, `cardBorder`, `inputBorder`, `buttonBorder`, and semantic tokens (`successBg`, `errorBg`, `warningBg`, `infoBg`, and their border/text variants).
`dk` is the raw dark palette, useful when you need dark-specific values regardless of mode (e.g. for overlay rendering).
***
## Next Steps
See all available components
Configure providers and chains
See styled examples in action
Follow integration best practices