# Paying from Solana & TON

On the Solana and TON FROM chains there is **no contract**. A swap is still the same four steps —
discover, quote, pay, track — but the payment is a **plain transfer to the treasury that carries
the `quoteId`**: in an SPL Memo instruction on Solana, as the text comment on TON. The backend
reads the transfer straight from the chain and matches it against the signed quote.

Because nothing on-chain validates the payment, the **transaction shape is the protocol**. Build
it exactly as described here, or run the scripts below as they are.

## The rules in short

- The transfer is sent by the wallet you quoted as `sender` (on Solana: the fee payer).
- The treasury receives **exactly `fromTotal`** of the quoted token. Nothing else moves to it.
- The memo / comment is the `quoteId` verbatim (`0x` + 64 hex), once.
- It lands before the quote's `deadline`. Nothing on-chain stops a late transfer — it becomes a
  refund case instead of an order.

A payment that breaks the payer / token / amount rule is held for operator review and is **not
refunded automatically**. The full table, with every outcome, is on
[Payment rules](https://docs.drip.tools/reference/lifecycle.md#payment-rules).

> **Tip: Timing**
>
> A quote lives 180 s. Request it right before you send; if fewer than ~20 s remain when you are
> about to broadcast, request a fresh one. On TON, set the message's `validUntil` to the quote
> `deadline` so the network drops a late broadcast for you.

## Solana

**FROM chain id `1399811149`** (`family: "solana"`). Tokens: native SOL (`0x000…0`), USDC and
USDT (base58 mints — read them from `/api/chains`). The treasury is `contracts.depository` on that
chain.

**Transaction shape**

- **Native SOL:** one `SystemProgram.transfer` from your wallet to the treasury for exactly
  `fromTotal` lamports.
- **SPL token (USDC / USDT):** a `TransferChecked` from your associated token account to the
  **treasury's associated token account** for exactly `fromTotal` units of the mint, preceded by
  an Associated Token Account **`CreateIdempotent`** for the treasury's account (a no-op when it
  exists; otherwise you fund its rent, ~0.002 SOL). Classic Token program only.
- **Memo:** an SPL Memo instruction (program `MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`;
  the v1 program is accepted too) whose data is the `quoteId` as UTF-8 text. No signer accounts
  are needed on the memo.
- Your wallet is the **fee payer**. Failed transactions are ignored. Payments are indexed at
  `finalized` commitment, typically 15–30 s after confirmation.

### Native SOL

Two instructions in one transaction — the transfer and the memo:

**JavaScript · @solana/web3.js** — excerpt from `examples/js/solana-native.mjs`
```js
// 3. Pay: transfer EXACTLY fromTotal to the treasury, plus the quoteId as a memo.
//    Your wallet is the fee payer — it must be the `sender` you quoted.
const tx = new Transaction().add(
  SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: treasury, lamports: BigInt(quote.fromTotal) }),
  new TransactionInstruction({ programId: MEMO_PROGRAM_ID, keys: [], data: Buffer.from(quote.quoteId, 'utf8') }),
)
if (DRY_RUN) {
  tx.feePayer = payer.publicKey
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash
  tx.sign(payer)
  console.log('DRY_RUN — signed tx', tx.serialize().length, 'bytes, signature', bs58.encode(tx.signature))
  process.exit(0)
}
const signature = await sendAndConfirmTransaction(connection, tx, [payer])
console.log('Paid', signature)
```

**Python · solana-py** — excerpt from `examples/python/solana_native.py`
```python
# 3. Pay: transfer EXACTLY fromTotal to the treasury + the quoteId as a memo.
#    Your wallet is the fee payer — it must be the `sender` you quoted.
async with AsyncClient(solana["rpc"][0]) as client:
    blockhash = (await client.get_latest_blockhash()).value.blockhash
    instructions = [
        transfer(TransferParams(from_pubkey=payer.pubkey(), to_pubkey=treasury, lamports=int(quote["fromTotal"]))),
        Instruction(MEMO_PROGRAM_ID, quote["quoteId"].encode(), []),
    ]
    message = Message.new_with_blockhash(instructions, payer.pubkey(), blockhash)
    tx = VersionedTransaction(message, [payer])
    if DRY_RUN:
        print("DRY_RUN — signed tx", len(bytes(tx)), "bytes, signature", tx.signatures[0])
        return
    signature = (await client.send_transaction(tx)).value
    await client.confirm_transaction(signature, "confirmed")
    print("Paid", signature)
```

**Full script — discover, quote, pay, track**

**JavaScript · @solana/web3.js** — `examples/js/solana-native.mjs`
```js
// Drip Tools — pay from Solana with native SOL (@solana/web3.js).
// A plain transfer to the treasury + the quoteId in an SPL Memo instruction. No program call.
// Run:      SOLANA_SECRET_KEY=<base58> node solana-native.mjs
// Dry run:  DRY_RUN=1 node solana-native.mjs   (real quote, builds + signs, sends nothing)
import {
  Connection, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction,
  sendAndConfirmTransaction,
} from '@solana/web3.js'
import bs58 from 'bs58'

const API = 'https://drip.tools/api'
const MEMO_PROGRAM_ID = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr')
const FROM_CHAIN_ID = 1399811149                                      // Solana mainnet
const TO_CHAIN_ID   = Number(process.env.TO_CHAIN_ID ?? 421614)       // Arbitrum Sepolia
const RECIPIENT     = process.env.RECIPIENT ?? '0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A' // ← YOUR testnet wallet
const TO_AMOUNT     = process.env.TO_AMOUNT ?? '5000000000000000000'  // 5 testnet ETH (wei)
const NATIVE        = '0x0000000000000000000000000000000000000000'
const DRY_RUN       = process.env.DRY_RUN === '1'
if (!DRY_RUN && RECIPIENT === '0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A') throw new Error('Set RECIPIENT to your own testnet wallet')

if (!DRY_RUN && !process.env.SOLANA_SECRET_KEY) throw new Error('Set SOLANA_SECRET_KEY')
const payer = process.env.SOLANA_SECRET_KEY ? Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_SECRET_KEY))
                                            : Keypair.generate()

// 1. Discover: RPC + the treasury (the pay target on Solana)
const chains = await fetch(`${API}/chains?unwrap=true`).then(r => r.json())
const solana = chains.find(c => c.chainId === String(FROM_CHAIN_ID))
const connection = new Connection(solana.rpc[0], 'confirmed')
const treasury = new PublicKey(solana.contracts.depository)

// 2. Quote — `sender` is your base58 public key, exactly as the wallet reports it
const quote = await fetch(`${API}/quote`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    chainId: FROM_CHAIN_ID, fromToken: NATIVE, sender: payer.publicKey.toBase58(),
    toChainId: TO_CHAIN_ID, toToken: NATIVE,
    recipients: [RECIPIENT], toAmounts: [TO_AMOUNT],
  }),
}).then(r => r.json())
if (quote.error) throw new Error(`${quote.code ?? 'ERROR'}: ${quote.error}`)
console.log(`Cost ${quote.fromTotal} lamports (~$${quote.usdTotal}) → treasury ${quote.vault}`)

// 3. Pay: transfer EXACTLY fromTotal to the treasury, plus the quoteId as a memo.
//    Your wallet is the fee payer — it must be the `sender` you quoted.
const tx = new Transaction().add(
  SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: treasury, lamports: BigInt(quote.fromTotal) }),
  new TransactionInstruction({ programId: MEMO_PROGRAM_ID, keys: [], data: Buffer.from(quote.quoteId, 'utf8') }),
)
if (DRY_RUN) {
  tx.feePayer = payer.publicKey
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash
  tx.sign(payer)
  console.log('DRY_RUN — signed tx', tx.serialize().length, 'bytes, signature', bs58.encode(tx.signature))
  process.exit(0)
}
const signature = await sendAndConfirmTransaction(connection, tx, [payer])
console.log('Paid', signature)

// 4. Track by the transaction signature (indexed at `finalized`, usually 15–30 s)
for (;;) {
  const { data } = await fetch(`${API}/order?from_tx_hash=${signature}`).then(r => r.json())
  if (data.length && data.every(o => o.status === 'completed' || o.status === 'failed')) {
    for (const o of data) console.log(`${o.to.address}: ${o.status} ${o.to.txHash ?? ''}`)
    break
  }
  await new Promise(r => setTimeout(r, 5000))
}
```

**Python · solana-py** — `examples/python/solana_native.py`
```python
"""Drip Tools — pay from Solana with native SOL (solana-py + solders).

A plain transfer to the treasury + the quoteId in an SPL Memo instruction. No program call.
Run:      SOLANA_SECRET_KEY=<base58> python solana_native.py
Dry run:  DRY_RUN=1 python solana_native.py   (real quote, builds + signs, sends nothing)
"""
import asyncio
import os

import requests
from solana.rpc.async_api import AsyncClient
from solders.instruction import Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.system_program import TransferParams, transfer
from solders.transaction import VersionedTransaction
from spl.memo.constants import MEMO_PROGRAM_ID

API = "https://drip.tools/api"
FROM_CHAIN_ID = 1399811149                                        # Solana mainnet
TO_CHAIN_ID = int(os.environ.get("TO_CHAIN_ID", 421614))          # Arbitrum Sepolia
RECIPIENT = os.environ.get("RECIPIENT", "0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A")  # ← YOUR testnet wallet
TO_AMOUNT = os.environ.get("TO_AMOUNT", "5000000000000000000")    # 5 testnet ETH (wei)
NATIVE = "0x0000000000000000000000000000000000000000"
DRY_RUN = os.environ.get("DRY_RUN") == "1"
if not DRY_RUN and RECIPIENT == "0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A":
    raise SystemExit("Set RECIPIENT to your own testnet wallet")

payer = Keypair.from_base58_string(os.environ["SOLANA_SECRET_KEY"]) if "SOLANA_SECRET_KEY" in os.environ else Keypair()
if not DRY_RUN and "SOLANA_SECRET_KEY" not in os.environ:
    raise SystemExit("Set SOLANA_SECRET_KEY")


async def main():
    # 1. Discover: RPC + the treasury (the pay target on Solana)
    chains = requests.get(f"{API}/chains", params={"unwrap": "true"}, timeout=30).json()
    solana = next(c for c in chains if c["chainId"] == str(FROM_CHAIN_ID))
    treasury = Pubkey.from_string(solana["contracts"]["depository"])

    # 2. Quote — `sender` is your base58 public key
    quote = requests.post(f"{API}/quote", json={
        "chainId": FROM_CHAIN_ID, "fromToken": NATIVE, "sender": str(payer.pubkey()),
        "toChainId": TO_CHAIN_ID, "toToken": NATIVE,
        "recipients": [RECIPIENT], "toAmounts": [TO_AMOUNT],
    }, timeout=30).json()
    if "error" in quote:
        raise SystemExit(f"{quote.get('code', 'ERROR')}: {quote['error']}")
    print(f"Cost {quote['fromTotal']} lamports (~${quote['usdTotal']}) → treasury {quote['vault']}")

    # 3. Pay: transfer EXACTLY fromTotal to the treasury + the quoteId as a memo.
    #    Your wallet is the fee payer — it must be the `sender` you quoted.
    async with AsyncClient(solana["rpc"][0]) as client:
        blockhash = (await client.get_latest_blockhash()).value.blockhash
        instructions = [
            transfer(TransferParams(from_pubkey=payer.pubkey(), to_pubkey=treasury, lamports=int(quote["fromTotal"]))),
            Instruction(MEMO_PROGRAM_ID, quote["quoteId"].encode(), []),
        ]
        message = Message.new_with_blockhash(instructions, payer.pubkey(), blockhash)
        tx = VersionedTransaction(message, [payer])
        if DRY_RUN:
            print("DRY_RUN — signed tx", len(bytes(tx)), "bytes, signature", tx.signatures[0])
            return
        signature = (await client.send_transaction(tx)).value
        await client.confirm_transaction(signature, "confirmed")
        print("Paid", signature)

    # 4. Track by the transaction signature (indexed at `finalized`, usually 15–30 s)
    while True:
        data = requests.get(f"{API}/order", params={"from_tx_hash": str(signature)}, timeout=30).json()["data"]
        if data and all(o["status"] in ("completed", "failed") for o in data):
            for o in data:
                print(f"{o['to']['address']}: {o['status']} {o['to']['txHash'] or ''}")
            break
        await asyncio.sleep(5)


asyncio.run(main())
```

### SPL token (USDC / USDT)

`fromToken` is the mint address from `/api/chains`; `quote.fromToken` echoes it. Keep a little SOL
in the paying wallet for the fee and the possible rent of the treasury's token account
(~0.003 SOL covers both). Three instructions: create the treasury's token account if missing,
`TransferChecked`, memo:

**JavaScript · @solana/spl-token** — excerpt from `examples/js/solana-spl.mjs`
```js
// 3. Pay: create the treasury's token account if missing (no-op otherwise), transfer EXACTLY
//    fromTotal with TransferChecked, and add the quoteId memo. Keep ~0.003 SOL for fee + rent.
const mint        = new PublicKey(quote.fromToken)
const payerAta    = getAssociatedTokenAddressSync(mint, payer.publicKey)
const treasuryAta = getAssociatedTokenAddressSync(mint, treasury)
const tx = new Transaction().add(
  createAssociatedTokenAccountIdempotentInstruction(payer.publicKey, treasuryAta, treasury, mint),
  createTransferCheckedInstruction(payerAta, mint, treasuryAta, payer.publicKey, BigInt(quote.fromTotal), token.decimals),
  new TransactionInstruction({ programId: MEMO_PROGRAM_ID, keys: [], data: Buffer.from(quote.quoteId, 'utf8') }),
)
if (DRY_RUN) {
  tx.feePayer = payer.publicKey
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash
  tx.sign(payer)
  console.log('DRY_RUN — signed tx', tx.serialize().length, 'bytes, signature', bs58.encode(tx.signature))
  process.exit(0)
}
const signature = await sendAndConfirmTransaction(connection, tx, [payer])
console.log('Paid', signature)
```

**Python · solana-py** — excerpt from `examples/python/solana_spl.py`
```python
# 3. Pay: create the treasury's token account if missing (no-op otherwise), transfer EXACTLY
#    fromTotal with TransferChecked, and add the quoteId memo. Keep ~0.003 SOL for fee + rent.
mint = Pubkey.from_string(quote["fromToken"])
payer_ata = get_associated_token_address(payer.pubkey(), mint)
treasury_ata = get_associated_token_address(treasury, mint)
async with AsyncClient(solana["rpc"][0]) as client:
    blockhash = (await client.get_latest_blockhash()).value.blockhash
    instructions = [
        create_idempotent_associated_token_account(payer.pubkey(), treasury, mint),
        transfer_checked(TransferCheckedParams(
            program_id=TOKEN_PROGRAM_ID, source=payer_ata, mint=mint, dest=treasury_ata,
            owner=payer.pubkey(), amount=int(quote["fromTotal"]), decimals=token["decimals"],
        )),
        Instruction(MEMO_PROGRAM_ID, quote["quoteId"].encode(), []),
    ]
    message = Message.new_with_blockhash(instructions, payer.pubkey(), blockhash)
    tx = VersionedTransaction(message, [payer])
    if DRY_RUN:
        print("DRY_RUN — signed tx", len(bytes(tx)), "bytes, signature", tx.signatures[0])
        return
    signature = (await client.send_transaction(tx)).value
    await client.confirm_transaction(signature, "confirmed")
    print("Paid", signature)
```

**Full script — discover, quote, pay, track**

**JavaScript · @solana/spl-token** — `examples/js/solana-spl.mjs`
```js
// Drip Tools — pay from Solana with an SPL token (USDC / USDT), @solana/web3.js + @solana/spl-token.
// TransferChecked to the treasury's associated token account + the quoteId in an SPL Memo.
// Run:      SOLANA_SECRET_KEY=<base58> node solana-spl.mjs
// Dry run:  DRY_RUN=1 node solana-spl.mjs
import {
  Connection, Keypair, PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction,
} from '@solana/web3.js'
import {
  getAssociatedTokenAddressSync,
  createAssociatedTokenAccountIdempotentInstruction,
  createTransferCheckedInstruction,
} from '@solana/spl-token'
import bs58 from 'bs58'

const API = 'https://drip.tools/api'
const MEMO_PROGRAM_ID = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr')
const FROM_CHAIN_ID = 1399811149                                      // Solana mainnet
const FROM_SYMBOL   = process.env.FROM_SYMBOL ?? 'USDC'               // USDC or USDT
const TO_CHAIN_ID   = Number(process.env.TO_CHAIN_ID ?? 421614)       // Arbitrum Sepolia
const RECIPIENT     = process.env.RECIPIENT ?? '0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A' // ← YOUR testnet wallet
const TO_AMOUNT     = process.env.TO_AMOUNT ?? '5000000000000000000'  // 5 testnet ETH (wei)
const NATIVE        = '0x0000000000000000000000000000000000000000'
const DRY_RUN       = process.env.DRY_RUN === '1'
if (!DRY_RUN && RECIPIENT === '0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A') throw new Error('Set RECIPIENT to your own testnet wallet')

if (!DRY_RUN && !process.env.SOLANA_SECRET_KEY) throw new Error('Set SOLANA_SECRET_KEY')
const payer = process.env.SOLANA_SECRET_KEY ? Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_SECRET_KEY))
                                            : Keypair.generate()

// 1. Discover: RPC, treasury, and the mint of the token you pay with
const chains = await fetch(`${API}/chains?unwrap=true`).then(r => r.json())
const solana = chains.find(c => c.chainId === String(FROM_CHAIN_ID))
const token = solana.tokens.find(t => t.symbol === FROM_SYMBOL)       // { address: <mint>, decimals }
const connection = new Connection(solana.rpc[0], 'confirmed')
const treasury = new PublicKey(solana.contracts.depository)

// 2. Quote — fromToken is the mint address from /api/chains
const quote = await fetch(`${API}/quote`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    chainId: FROM_CHAIN_ID, fromToken: token.address, sender: payer.publicKey.toBase58(),
    toChainId: TO_CHAIN_ID, toToken: NATIVE,
    recipients: [RECIPIENT], toAmounts: [TO_AMOUNT],
  }),
}).then(r => r.json())
if (quote.error) throw new Error(`${quote.code ?? 'ERROR'}: ${quote.error}`)
console.log(`Cost ${quote.fromTotal} ${FROM_SYMBOL} base units (~$${quote.usdTotal})`)

// 3. Pay: create the treasury's token account if missing (no-op otherwise), transfer EXACTLY
//    fromTotal with TransferChecked, and add the quoteId memo. Keep ~0.003 SOL for fee + rent.
const mint        = new PublicKey(quote.fromToken)
const payerAta    = getAssociatedTokenAddressSync(mint, payer.publicKey)
const treasuryAta = getAssociatedTokenAddressSync(mint, treasury)
const tx = new Transaction().add(
  createAssociatedTokenAccountIdempotentInstruction(payer.publicKey, treasuryAta, treasury, mint),
  createTransferCheckedInstruction(payerAta, mint, treasuryAta, payer.publicKey, BigInt(quote.fromTotal), token.decimals),
  new TransactionInstruction({ programId: MEMO_PROGRAM_ID, keys: [], data: Buffer.from(quote.quoteId, 'utf8') }),
)
if (DRY_RUN) {
  tx.feePayer = payer.publicKey
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash
  tx.sign(payer)
  console.log('DRY_RUN — signed tx', tx.serialize().length, 'bytes, signature', bs58.encode(tx.signature))
  process.exit(0)
}
const signature = await sendAndConfirmTransaction(connection, tx, [payer])
console.log('Paid', signature)

// 4. Track by the transaction signature
for (;;) {
  const { data } = await fetch(`${API}/order?from_tx_hash=${signature}`).then(r => r.json())
  if (data.length && data.every(o => o.status === 'completed' || o.status === 'failed')) {
    for (const o of data) console.log(`${o.to.address}: ${o.status} ${o.to.txHash ?? ''}`)
    break
  }
  await new Promise(r => setTimeout(r, 5000))
}
```

**Python · solana-py** — `examples/python/solana_spl.py`
```python
"""Drip Tools — pay from Solana with an SPL token (USDC / USDT), solana-py + solders.

TransferChecked to the treasury's associated token account + the quoteId in an SPL Memo.
Run:      SOLANA_SECRET_KEY=<base58> python solana_spl.py
Dry run:  DRY_RUN=1 python solana_spl.py
"""
import asyncio
import os

import requests
from solana.rpc.async_api import AsyncClient
from solders.instruction import Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction
from spl.memo.constants import MEMO_PROGRAM_ID
from spl.token.constants import TOKEN_PROGRAM_ID
from spl.token.instructions import (
    create_idempotent_associated_token_account, get_associated_token_address, transfer_checked,
)
from spl.token.models import TransferCheckedParams

API = "https://drip.tools/api"
FROM_CHAIN_ID = 1399811149                                        # Solana mainnet
FROM_SYMBOL = os.environ.get("FROM_SYMBOL", "USDC")               # USDC or USDT
TO_CHAIN_ID = int(os.environ.get("TO_CHAIN_ID", 421614))          # Arbitrum Sepolia
RECIPIENT = os.environ.get("RECIPIENT", "0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A")  # ← YOUR testnet wallet
TO_AMOUNT = os.environ.get("TO_AMOUNT", "5000000000000000000")    # 5 testnet ETH (wei)
NATIVE = "0x0000000000000000000000000000000000000000"
DRY_RUN = os.environ.get("DRY_RUN") == "1"
if not DRY_RUN and RECIPIENT == "0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A":
    raise SystemExit("Set RECIPIENT to your own testnet wallet")

payer = Keypair.from_base58_string(os.environ["SOLANA_SECRET_KEY"]) if "SOLANA_SECRET_KEY" in os.environ else Keypair()
if not DRY_RUN and "SOLANA_SECRET_KEY" not in os.environ:
    raise SystemExit("Set SOLANA_SECRET_KEY")


async def main():
    # 1. Discover: RPC, treasury, and the mint of the token you pay with
    chains = requests.get(f"{API}/chains", params={"unwrap": "true"}, timeout=30).json()
    solana = next(c for c in chains if c["chainId"] == str(FROM_CHAIN_ID))
    token = next(t for t in solana["tokens"] if t["symbol"] == FROM_SYMBOL)   # address = mint, decimals
    treasury = Pubkey.from_string(solana["contracts"]["depository"])

    # 2. Quote — fromToken is the mint address from /api/chains
    quote = requests.post(f"{API}/quote", json={
        "chainId": FROM_CHAIN_ID, "fromToken": token["address"], "sender": str(payer.pubkey()),
        "toChainId": TO_CHAIN_ID, "toToken": NATIVE,
        "recipients": [RECIPIENT], "toAmounts": [TO_AMOUNT],
    }, timeout=30).json()
    if "error" in quote:
        raise SystemExit(f"{quote.get('code', 'ERROR')}: {quote['error']}")
    print(f"Cost {quote['fromTotal']} {FROM_SYMBOL} base units (~${quote['usdTotal']})")

    # 3. Pay: create the treasury's token account if missing (no-op otherwise), transfer EXACTLY
    #    fromTotal with TransferChecked, and add the quoteId memo. Keep ~0.003 SOL for fee + rent.
    mint = Pubkey.from_string(quote["fromToken"])
    payer_ata = get_associated_token_address(payer.pubkey(), mint)
    treasury_ata = get_associated_token_address(treasury, mint)
    async with AsyncClient(solana["rpc"][0]) as client:
        blockhash = (await client.get_latest_blockhash()).value.blockhash
        instructions = [
            create_idempotent_associated_token_account(payer.pubkey(), treasury, mint),
            transfer_checked(TransferCheckedParams(
                program_id=TOKEN_PROGRAM_ID, source=payer_ata, mint=mint, dest=treasury_ata,
                owner=payer.pubkey(), amount=int(quote["fromTotal"]), decimals=token["decimals"],
            )),
            Instruction(MEMO_PROGRAM_ID, quote["quoteId"].encode(), []),
        ]
        message = Message.new_with_blockhash(instructions, payer.pubkey(), blockhash)
        tx = VersionedTransaction(message, [payer])
        if DRY_RUN:
            print("DRY_RUN — signed tx", len(bytes(tx)), "bytes, signature", tx.signatures[0])
            return
        signature = (await client.send_transaction(tx)).value
        await client.confirm_transaction(signature, "confirmed")
        print("Paid", signature)

    # 4. Track by the transaction signature
    while True:
        data = requests.get(f"{API}/order", params={"from_tx_hash": str(signature)}, timeout=30).json()["data"]
        if data and all(o["status"] in ("completed", "failed") for o in data):
            for o in data:
                print(f"{o['to']['address']}: {o['status']} {o['to']['txHash'] or ''}")
            break
        await asyncio.sleep(5)


asyncio.run(main())
```

### Browser wallets

With a Wallet Standard wallet (Phantom, Solflare, …) build the same `Transaction` with the
wallet's public key as `feePayer`, then `signAndSendTransaction`. The wallet must be the one you
quoted as `sender`.

## TON

**FROM chain id `1667471768`** (`family: "ton"`). Token: native Toncoin only (`GRAM`, address
`0x000…0`, 9 decimals). The treasury is `contracts.depository` on that chain, given in the
non-bounceable `UQ…` form.

**Transaction shape**

- One **internal transfer** from your wallet to the treasury for exactly `fromTotal`
  **nanotons**.
- **Non-bounceable** (`bounce: false`). The treasury is a plain wallet; use the `UQ…` address
  exactly as returned.
- **Text comment** = the `quoteId` (`0x` + 64 hex). A standard comment cell: 32-bit op `0`
  followed by the UTF-8 text, in a single cell — every wallet's "comment" / "memo" field
  produces exactly this.
- Set the message's **`validUntil`** to the quote `deadline` (or earlier) so the network refuses
  a late broadcast.
- Send with **mode 1 or 3** (fees paid separately, `SendMode.PAY_GAS_SEPARATELY`). With mode 0 the
  forward fee comes out of the value, the treasury receives less than `fromTotal`, and the payment
  is held for review, not refunded. Wallets and TON Connect do this by default; `@ton/ton` does
  not — pass `sendMode` explicitly, as the script below does.
- Payments are indexed from the treasury's transactions, typically 10–20 s after they land.

`sender` in the quote request can be any form of your address (`EQ…`, `UQ…`, raw `0:…`); the
API normalizes it to the `UQ…` form, which is also how the payer appears on the order.

> **Tip: Wallet versions**
>
> One mnemonic gives a different address per wallet contract version. The JavaScript script uses
> **W5 (v5r1)**, the default of today's wallets, and prints the address it is about to pay from;
> set `TON_WALLET=v4r2` for a v4 wallet. The Python script (pytoniq) supports v4r2 only. Fund the
> address the script prints, not the one your wallet app shows, if they differ.

### The message

One `internal` message: non-bounceable, exactly `fromTotal` nanotons, the `quoteId` as the text
comment, valid until the quote `deadline`:

**JavaScript · @ton/ton** — excerpt from `examples/js/ton.mjs`
```js
// 3. Pay: ONE non-bounceable transfer of EXACTLY fromTotal, text comment = quoteId,
//    valid until the quote deadline so the network drops a late message.
const messages = [
  internal({
    to: Address.parse(treasury),
    value: BigInt(quote.fromTotal),          // nanotons
    bounce: false,                           // the treasury is a plain wallet
    body: quote.quoteId,                     // a string body becomes the text comment
  }),
]
if (DRY_RUN) {
  const cell = wallet.createTransfer({ seqno: 1, secretKey: keyPair.secretKey, timeout: quote.deadline, sendMode: SEND_MODE, messages })
  console.log('DRY_RUN — signed external message', cell.toBoc().toString('base64').length, 'chars (base64)')
  process.exit(0)
}
// toncenter allows ~1 request/s without a key: retry on 429 (or set TONCENTER_API_KEY)
const retry = async (fn, tries = 6) => {
  for (let i = 0; ; i++) {
    try { return await fn() } catch (e) {
      if (i >= tries - 1 || e?.response?.status !== 429) throw e
      await new Promise(r => setTimeout(r, 1500 * (i + 1)))
    }
  }
}
const client = new TonClient({
  endpoint: 'https://toncenter.com/api/v2/jsonRPC',
  apiKey: process.env.TONCENTER_API_KEY,     // optional; raises the rate limit
})
const contract = client.open(wallet)
const seqno = await retry(() => contract.getSeqno())
await retry(() => contract.sendTransfer({ seqno, secretKey: keyPair.secretKey, timeout: quote.deadline, sendMode: SEND_MODE, messages }))
console.log('Sent; tracking by quoteId', quote.quoteId)
```

**Python · pytoniq** — excerpt from `examples/python/ton.py`
```python
# 3. Pay: ONE non-bounceable transfer of EXACTLY fromTotal, text comment = quoteId,
#    valid until the quote deadline so the network drops a late message.
comment = begin_cell().store_uint(0, 32).store_snake_string(quote["quoteId"]).end_cell()
message = WalletV4R2.create_wallet_internal_message(
    destination=Address(treasury), value=int(quote["fromTotal"]), body=comment, bounce=False,
    send_mode=3,  # fees paid separately — mode 0 would deliver less than fromTotal and the payment would be held
)
if DRY_RUN:
    body = WalletV4R2.raw_create_transfer_msg(private_key=private_key, seqno=1, wallet_id=wallet_id,
                                              messages=[message], valid_until=int(quote["deadline"]))
    print("DRY_RUN — signed external message", len(body.to_boc()), "bytes, from", sender)
    return
provider = LiteBalancer.from_mainnet_config(trust_level=2)
await provider.start_up()
wallet = await WalletV4R2.from_data(provider, public_key=public_key, private_key=private_key)
seqno = await wallet.get_seqno()
body = WalletV4R2.raw_create_transfer_msg(private_key=private_key, seqno=seqno, wallet_id=wallet_id,
                                          messages=[message], valid_until=int(quote["deadline"]))
await wallet.send_external(body=body)
await provider.close_all()
print("Sent; tracking by quoteId", quote["quoteId"])
```

**TON Connect (browser)** — `examples/js/ton-connect.js`
```js
// Drip Tools — pay from TON in the browser with TON Connect.
// Same message as ton.mjs: one non-bounceable transfer, comment = quoteId, valid until the deadline.
import { beginCell } from '@ton/core'

// quote: the response of POST /api/quote, requested with sender = the connected wallet's address
// treasury: contracts.depository of the TON chain from GET /api/chains (UQ… form)
export async function payWithTonConnect(tonConnectUI, quote, treasury) {
  const body = beginCell().storeUint(0, 32).storeStringTail(quote.quoteId).endCell()
  await tonConnectUI.sendTransaction({
    validUntil: quote.deadline,
    messages: [{
      address: treasury,                       // UQ… → the wallet sends it non-bounceable
      amount:  quote.fromTotal,                // nanotons, as a string
      payload: body.toBoc().toString('base64'),
    }],
  })
  // then poll GET /api/order?from_tx_hash=<quote.quoteId>
}
```

**Full script — discover, quote, pay, track**

**JavaScript · @ton/ton** — `examples/js/ton.mjs`
```js
// Drip Tools — pay from TON with native Toncoin (@ton/ton).
// A non-bounceable transfer to the treasury with the quoteId as the text comment. No contract.
// Run:      TON_MNEMONIC="word1 … word24" TONCENTER_API_KEY=… node ton.mjs
// Wallet:   W5 (v5r1, the default of today's wallets) unless TON_WALLET=v4r2 — the address differs per version
// Dry run:  DRY_RUN=1 node ton.mjs   (real quote, builds + signs the message offline, sends nothing)
import { TonClient, WalletContractV4, WalletContractV5R1, internal } from '@ton/ton'
import { mnemonicToPrivateKey, mnemonicNew } from '@ton/crypto'
import { Address, SendMode } from '@ton/core'

const API = 'https://drip.tools/api'
const FROM_CHAIN_ID = 1667471768                                      // TON mainnet
const TO_CHAIN_ID   = Number(process.env.TO_CHAIN_ID ?? 421614)       // Arbitrum Sepolia
const RECIPIENT     = process.env.RECIPIENT ?? '0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A' // ← YOUR testnet wallet
const TO_AMOUNT     = process.env.TO_AMOUNT ?? '5000000000000000000'  // 5 testnet ETH (wei)
const NATIVE        = '0x0000000000000000000000000000000000000000'
const DRY_RUN       = process.env.DRY_RUN === '1'
// Fees on top of the value: with the default mode 0 the forward fee comes OUT of the value, the
// treasury receives less than fromTotal, and the payment is held for review instead of honoured.
const SEND_MODE     = SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS
if (!DRY_RUN && RECIPIENT === '0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A') throw new Error('Set RECIPIENT to your own testnet wallet')

if (!DRY_RUN && !process.env.TON_MNEMONIC) throw new Error('Set TON_MNEMONIC')
const mnemonic = process.env.TON_MNEMONIC ? process.env.TON_MNEMONIC.split(' ') : await mnemonicNew()
const keyPair = await mnemonicToPrivateKey(mnemonic)
const WALLET  = process.env.TON_WALLET ?? 'v5r1'
const wallet  = WALLET === 'v4r2' ? WalletContractV4.create({ workchain: 0, publicKey: keyPair.publicKey })
                                 : WalletContractV5R1.create({ workchain: 0, publicKey: keyPair.publicKey })

// 1. Discover the treasury (the pay target on TON, given in the non-bounceable UQ… form)
const chains = await fetch(`${API}/chains?unwrap=true`).then(r => r.json())
const ton = chains.find(c => c.chainId === String(FROM_CHAIN_ID))
const treasury = ton.contracts.depository

// 2. Quote — `sender` is your wallet address in any form (shown here as UQ…)
const sender = wallet.address.toString({ bounceable: false, urlSafe: true })
console.log(`Wallet ${sender} (${WALLET})`)
const quote = await fetch(`${API}/quote`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    chainId: FROM_CHAIN_ID, fromToken: NATIVE, sender,
    toChainId: TO_CHAIN_ID, toToken: NATIVE,
    recipients: [RECIPIENT], toAmounts: [TO_AMOUNT],
  }),
}).then(r => r.json())
if (quote.error) throw new Error(`${quote.code ?? 'ERROR'}: ${quote.error}`)
console.log(`Cost ${quote.fromTotal} nanotons (~$${quote.usdTotal}) → treasury ${treasury}`)

// 3. Pay: ONE non-bounceable transfer of EXACTLY fromTotal, text comment = quoteId,
//    valid until the quote deadline so the network drops a late message.
const messages = [
  internal({
    to: Address.parse(treasury),
    value: BigInt(quote.fromTotal),          // nanotons
    bounce: false,                           // the treasury is a plain wallet
    body: quote.quoteId,                     // a string body becomes the text comment
  }),
]
if (DRY_RUN) {
  const cell = wallet.createTransfer({ seqno: 1, secretKey: keyPair.secretKey, timeout: quote.deadline, sendMode: SEND_MODE, messages })
  console.log('DRY_RUN — signed external message', cell.toBoc().toString('base64').length, 'chars (base64)')
  process.exit(0)
}
// toncenter allows ~1 request/s without a key: retry on 429 (or set TONCENTER_API_KEY)
const retry = async (fn, tries = 6) => {
  for (let i = 0; ; i++) {
    try { return await fn() } catch (e) {
      if (i >= tries - 1 || e?.response?.status !== 429) throw e
      await new Promise(r => setTimeout(r, 1500 * (i + 1)))
    }
  }
}
const client = new TonClient({
  endpoint: 'https://toncenter.com/api/v2/jsonRPC',
  apiKey: process.env.TONCENTER_API_KEY,     // optional; raises the rate limit
})
const contract = client.open(wallet)
const seqno = await retry(() => contract.getSeqno())
await retry(() => contract.sendTransfer({ seqno, secretKey: keyPair.secretKey, timeout: quote.deadline, sendMode: SEND_MODE, messages }))
console.log('Sent; tracking by quoteId', quote.quoteId)

// 4. Track by the quoteId — your wallet does not learn the treasury-side tx hash
for (;;) {
  const { data } = await fetch(`${API}/order?from_tx_hash=${quote.quoteId}`).then(r => r.json())
  if (data.length && data.every(o => o.status === 'completed' || o.status === 'failed')) {
    for (const o of data) console.log(`${o.to.address}: ${o.status} ${o.to.txHash ?? ''}`)
    break
  }
  await new Promise(r => setTimeout(r, 5000))
}
```

**Python · pytoniq** — `examples/python/ton.py`
```python
"""Drip Tools — pay from TON with native Toncoin (pytoniq).

A non-bounceable transfer to the treasury with the quoteId as the text comment. No contract.
Run:      TON_MNEMONIC="word1 … word24" python ton.py
Dry run:  DRY_RUN=1 python ton.py   (real quote, builds the message offline, sends nothing)
"""
import asyncio
import os
import time

import requests
from pytoniq import LiteBalancer, WalletV4R2
from pytoniq.contract.wallets.wallet import WALLET_V4_R2_CODE
from pytoniq_core import Address, StateInit, begin_cell
from pytoniq_core.crypto.keys import mnemonic_new, mnemonic_to_private_key

API = "https://drip.tools/api"
FROM_CHAIN_ID = 1667471768                                        # TON mainnet
TO_CHAIN_ID = int(os.environ.get("TO_CHAIN_ID", 421614))          # Arbitrum Sepolia
RECIPIENT = os.environ.get("RECIPIENT", "0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A")  # ← YOUR testnet wallet
TO_AMOUNT = os.environ.get("TO_AMOUNT", "5000000000000000000")    # 5 testnet ETH (wei)
NATIVE = "0x0000000000000000000000000000000000000000"
DRY_RUN = os.environ.get("DRY_RUN") == "1"
if not DRY_RUN and RECIPIENT == "0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A":
    raise SystemExit("Set RECIPIENT to your own testnet wallet")

mnemonic = os.environ["TON_MNEMONIC"].split() if "TON_MNEMONIC" in os.environ else mnemonic_new(24)
if not DRY_RUN and "TON_MNEMONIC" not in os.environ:
    raise SystemExit("Set TON_MNEMONIC")


async def main():
    # 1. Discover the treasury (the pay target on TON, given in the non-bounceable UQ… form)
    chains = requests.get(f"{API}/chains", params={"unwrap": "true"}, timeout=30).json()
    ton = next(c for c in chains if c["chainId"] == str(FROM_CHAIN_ID))
    treasury = ton["contracts"]["depository"]

    # Your wallet (v4r2). The address is derived offline; a lite-client is only needed to send.
    public_key, private_key = mnemonic_to_private_key(mnemonic)
    wallet_id = 698983191
    state_init = StateInit(code=WALLET_V4_R2_CODE, data=WalletV4R2.create_data_cell(public_key, wallet_id))
    address = Address((0, state_init.serialize().hash))
    sender = address.to_str(is_bounceable=False)                   # UQ… form (any form is accepted)

    # 2. Quote
    quote = requests.post(f"{API}/quote", json={
        "chainId": FROM_CHAIN_ID, "fromToken": NATIVE, "sender": sender,
        "toChainId": TO_CHAIN_ID, "toToken": NATIVE,
        "recipients": [RECIPIENT], "toAmounts": [TO_AMOUNT],
    }, timeout=30).json()
    if "error" in quote:
        raise SystemExit(f"{quote.get('code', 'ERROR')}: {quote['error']}")
    print(f"Cost {quote['fromTotal']} nanotons (~${quote['usdTotal']}) → treasury {treasury}")

    # 3. Pay: ONE non-bounceable transfer of EXACTLY fromTotal, text comment = quoteId,
    #    valid until the quote deadline so the network drops a late message.
    comment = begin_cell().store_uint(0, 32).store_snake_string(quote["quoteId"]).end_cell()
    message = WalletV4R2.create_wallet_internal_message(
        destination=Address(treasury), value=int(quote["fromTotal"]), body=comment, bounce=False,
        send_mode=3,  # fees paid separately — mode 0 would deliver less than fromTotal and the payment would be held
    )
    if DRY_RUN:
        body = WalletV4R2.raw_create_transfer_msg(private_key=private_key, seqno=1, wallet_id=wallet_id,
                                                  messages=[message], valid_until=int(quote["deadline"]))
        print("DRY_RUN — signed external message", len(body.to_boc()), "bytes, from", sender)
        return
    provider = LiteBalancer.from_mainnet_config(trust_level=2)
    await provider.start_up()
    wallet = await WalletV4R2.from_data(provider, public_key=public_key, private_key=private_key)
    seqno = await wallet.get_seqno()
    body = WalletV4R2.raw_create_transfer_msg(private_key=private_key, seqno=seqno, wallet_id=wallet_id,
                                              messages=[message], valid_until=int(quote["deadline"]))
    await wallet.send_external(body=body)
    await provider.close_all()
    print("Sent; tracking by quoteId", quote["quoteId"])

    # 4. Track by the quoteId — your wallet does not learn the treasury-side tx hash
    while True:
        data = requests.get(f"{API}/order", params={"from_tx_hash": quote["quoteId"]}, timeout=30).json()["data"]
        if data and all(o["status"] in ("completed", "failed") for o in data):
            for o in data:
                print(f"{o['to']['address']}: {o['status']} {o['to']['txHash'] or ''}")
            break
        time.sleep(5)


asyncio.run(main())
```

### Tracking on TON

Your wallet returns only the hash of the **external** message it broadcast, which is not the hash
of the treasury-side transaction the backend records. Pass the **`quoteId`** as `from_tx_hash`
instead — [`GET /api/order?from_tx_hash=<quoteId>`](https://docs.drip.tools/api/orders.md#get-api-order) resolves it once
the payment is honoured. The order's `from.txHash` is then the treasury-side transaction hash
(64 hex characters), which also works for every later lookup. A payment that was **refused**
(expired, no readable comment) never consumes its quote, so the `quoteId` lookup stays empty —
its refund shows up in the payer's history, `GET /api/orders?address=<UQ…>&view=batches`.

**JavaScript · @ton/ton** — excerpt from `examples/js/ton.mjs`
```js
// 4. Track by the quoteId — your wallet does not learn the treasury-side tx hash
for (;;) {
  const { data } = await fetch(`${API}/order?from_tx_hash=${quote.quoteId}`).then(r => r.json())
  if (data.length && data.every(o => o.status === 'completed' || o.status === 'failed')) {
    for (const o of data) console.log(`${o.to.address}: ${o.status} ${o.to.txHash ?? ''}`)
    break
  }
  await new Promise(r => setTimeout(r, 5000))
}
```

**Python · pytoniq** — excerpt from `examples/python/ton.py`
```python
# 4. Track by the quoteId — your wallet does not learn the treasury-side tx hash
while True:
    data = requests.get(f"{API}/order", params={"from_tx_hash": quote["quoteId"]}, timeout=30).json()["data"]
    if data and all(o["status"] in ("completed", "failed") for o in data):
        for o in data:
            print(f"{o['to']['address']}: {o['status']} {o['to']['txHash'] or ''}")
        break
    time.sleep(5)
```

## Checklist

- [ ] `sender` in the quote = the wallet that signs and pays the fee.
- [ ] Amount = `fromTotal`, in base units, exactly.
- [ ] `quoteId` in the memo / comment, nothing else, once.
- [ ] Only the quoted token moves to the treasury; no other transfers in the transaction.
- [ ] Broadcast with plenty of the 180 s left; on TON, `validUntil = deadline`.
- [ ] Track by the Solana signature, or by the `quoteId` on TON.
