# Falcon submission methods (/falcon/submission)

> Falcon JSON-RPC, /binary, /plaintext and QUIC reference: request shapes, size limits, options and exactly what each success response proves.



Every route takes one signed Solana transaction per request. Which one you want depends on what your client is already holding, and on whether you need an answer back.

| Route                            | Request body                                              | Body limit                 | Success                              | Preflight available |
| -------------------------------- | --------------------------------------------------------- | -------------------------- | ------------------------------------ | ------------------- |
| JSON-RPC `sendTransaction`       | JSON-RPC envelope, transaction as base58 or base64 string | 3,072 bytes                | JSON `result` = base58 signature     | Yes                 |
| `POST /binary`                   | Raw serialized transaction bytes                          | 1,232 bytes                | `200`, `text/plain` base58 signature | No                  |
| `POST /plaintext`                | Base64 transaction text                                   | 2,048 bytes                | `200`, `text/plain` base58 signature | No                  |
| QUIC (`falcon-client`)           | Serialized transaction on a stream or datagram            | 1,232 bytes                | `Ok(())`                             | No                  |
| [Falcon native UDP](/falcon/udp) | API key prefix + raw transaction bytes                    | 1,232 bytes of transaction | Nothing                              | No                  |

The decoded transaction is capped at **1,232 bytes** everywhere; the larger JSON-RPC and `/plaintext` limits cover the envelope and the base64 expansion on top of it.

<Callout type="warn" title="Acceptance is not confirmation">
  Falcon answers the moment the transaction validates and the forward is dispatched. No transport reports a forwarding failure: the outcome is discarded and you get the signature either way. Confirm it on a normal Solana RPC.
</Callout>

## Authenticate

| Transport       | How the UUID is carried                                                        |
| --------------- | ------------------------------------------------------------------------------ |
| All HTTP routes | `?api-key=YOUR_API_KEY_UUID` query parameter                                   |
| QUIC            | The SDK puts the UUID in the CN of a self-signed client certificate at connect |
| Native UDP      | The first 16 bytes (or 36 ASCII characters) of every datagram                  |

```text
http://fra.falcon.wtf/binary?api-key=00000000-0000-0000-0000-000000000000
```

Anything that stores a full URL stores your key with it; [Telemetry](/falcon/telemetry) covers what not to record.

## JSON-RPC

`POST` to the region root. There are two methods.

| Method            | Returns                                                 |
| ----------------- | ------------------------------------------------------- |
| `sendTransaction` | Base58 signature string                                 |
| `getVersion`      | `{"solana-core":"falcon/<version>","feature-set":null}` |

Anything else comes back as `-32601 method not found`. There are no batch arrays; send a single request object.

### `sendTransaction` parameters

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sendTransaction",
  "params": [
    "SIGNED_TRANSACTION_BASE64",
    { "encoding": "base64", "skipPreflight": true }
  ]
}
```

| Option                | Accepted values                       | Default  | Notes                                                                |
| --------------------- | ------------------------------------- | -------- | -------------------------------------------------------------------- |
| `encoding`            | `base58`, `base64`                    | `base58` | Any other value → `-32602 unsupported encoding`                      |
| `skipPreflight`       | `true`, `false`                       | `true`   | `false` makes Falcon simulate before forwarding, which costs latency |
| `preflightCommitment` | `processed`, `confirmed`, `finalized` | unset    | Only used when `skipPreflight` is `false`                            |
| `minContextSlot`      | non-negative integer                  | unset    | Only used when `skipPreflight` is `false`                            |
| `maxRetries`          | —                                     | —        | Accepted and ignored; Falcon does not re-drive submissions for you   |

Send a base64 string without `"encoding":"base64"` and you get `-32602 transaction string is not valid base58`.

### Preflight

Set `skipPreflight: false` and Falcon simulates the transaction, signature verification included, against an upstream RPC before forwarding. A short server-side timeout bounds the wait.

| Preflight outcome              | Response                                                                                                                             |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| Simulation returns an error    | `-32002`, message `Transaction simulation failed: <error>`, `data` = the full simulation result including `logs` and `unitsConsumed` |
| Upstream RPC errors            | `-32603 preflight check failed: RPC node returned an error`                                                                          |
| Simulation exceeds the timeout | `-32603 preflight check timed out`                                                                                                   |

None of those forward the transaction.

### Reading responses

```json
{"jsonrpc":"2.0","result":"5Yv...signature...","id":1}
```

JSON-RPC errors come back with HTTP `200` and an `error` object. Two of them do carry a matching status: `401` with `-32000 unauthorized`, and `429` with `-32001 rate limit exceeded`. The full table is on [Falcon errors & retries](/falcon/errors).

## HTTP `/binary`

For when you're already holding serialized transaction bytes. No envelope, no length prefix, no key in the body.

```bash
curl -sS -X POST "http://fra.falcon.wtf/binary?api-key=${FALCON_API_KEY}" \
  -H 'Content-Type: application/octet-stream' \
  --data-binary @transaction.bin
```

| Property     | Value                                                                                 |
| ------------ | ------------------------------------------------------------------------------------- |
| Body         | Raw serialized transaction                                                            |
| Maximum body | 1,232 bytes; a larger body is `413 request body too large`                            |
| Success      | `200`, `text/plain`, base58 signature                                                 |
| Rejections   | `400` with the reason as plain text, e.g. `transaction does not include required tip` |

## HTTP `/plaintext`

Base64 transaction text, no JSON envelope. Whitespace around it is trimmed for you.

```bash
curl -sS -X POST "http://fra.falcon.wtf/plaintext?api-key=${FALCON_API_KEY}" \
  -H 'Content-Type: text/plain' \
  --data "${TX_BASE64}"
```

| Property                    | Value                                             |
| --------------------------- | ------------------------------------------------- |
| Body                        | Base64 text                                       |
| Maximum body                | 2,048 bytes → `413 request body too large`        |
| Maximum decoded transaction | 1,232 bytes → `413 decoded transaction too large` |
| Invalid base64              | `400 invalid base64`                              |
| Non-UTF-8 body              | `400 invalid utf8`                                |
| Success                     | `200`, `text/plain`, base58 signature             |

Both raw routes check size, signatures and tip; neither runs preflight.

## QUIC with `falcon-client`

The published Rust SDK handles the persistent connection, serialization, authentication and one reconnect-and-retry for you.

```toml
[dependencies]
falcon-client = "0.1"
uuid          = "1"
```

```rust
use {falcon_client::FalconClient, uuid::Uuid};

let api_key = Uuid::parse_str("YOUR_API_KEY_UUID")?;
let mut client = FalconClient::connect("fra.falcon.wtf:5000", api_key).await?;

client.send_transaction(&transaction).await?;
```

Other entry points, if you need them:

| Call                                                                | Use for                                                         |
| ------------------------------------------------------------------- | --------------------------------------------------------------- |
| `send_transaction(&VersionedTransaction)`                           | The normal path; the SDK serializes for you                     |
| `send_transaction_payload(&[u8])` / `send_transaction_bytes(Bytes)` | Transactions you already serialized                             |
| `connect_with_bind(addr, key, local_addr)`                          | Pinning a fixed local UDP port for firewall rules               |
| `set_send_timeout(Duration)`                                        | Bounding the stream-ack wait (default 100 ms, stream mode only) |
| `is_connected()`                                                    | Liveness checks; the SDK reconnects on send failure anyway      |

### Transport modes

<Tabs items={['Stream (default)', 'Datagram']}>
  <Tab>
    `TransportMode::Stream` queues the transaction as a QUIC datagram **and** sends it on a bidirectional stream that comes back with a server ack.

    A server rejection on the stream reaches you as a `SubmitError`. But if the datagram was queued and the stream path then fails without a server rejection (a timeout, a stream error), you still get `Ok(())`. So read `Ok(())` here as "the server accepted it, or it's on the wire and the ack got lost".
  </Tab>

  <Tab>
    `TransportMode::Datagram` sends one fire-and-forget QUIC datagram: no stream, no ack, no server error.

    ```rust
    use falcon_client::TransportMode;

    client.set_transport_mode(TransportMode::Datagram);
    client.send_transaction(&transaction).await?;
    ```

    Here `Ok(())` means only that the packet was queued on your machine. The SDK does check the payload against the path MTU first and errors locally if it won't fit.

    Switching modes takes effect on your next send, with no reconnect.
  </Tab>
</Tabs>

<Accordions>
  <Accordion title="QUIC wire details for non-Rust clients">
    `falcon-client` is Apache-2.0, and its wire behaviour is fixed:

    | Element                      | Value                                                                                 |
    | ---------------------------- | ------------------------------------------------------------------------------------- |
    | ALPN                         | `falcon-tx`                                                                           |
    | Server name in the handshake | `falcon`                                                                              |
    | Client authentication        | Self-signed client certificate whose CN is the API key UUID                           |
    | Server certificate           | Self-signed; the reference client verifies the handshake signature but not a CA chain |
    | Stream request               | One byte `0x01`, then the serialized transaction                                      |
    | Stream response              | Exactly two bytes: `0x01`, then a status code                                         |
    | Status code `0x00`           | Accepted                                                                              |
    | Status codes `0x01`–`0x08`   | Rejected; see [SubmitError variants](/falcon/errors#quic-rejections)                  |
    | Datagram request             | The serialized transaction alone, no prefix                                           |

    The serialized transaction is the standard Solana wire encoding, the same bytes you'd post to `/binary`.
  </Accordion>
</Accordions>

## Delivery semantics

HTTP and QUIC stream mode acknowledge **acceptance**. QUIC datagram mode and native UDP acknowledge nothing.

Falcon keeps a signature-keyed cache of recent submissions and suppresses a duplicate, but only once that signature has already been delivered. That's why resending the same signed bytes while the blockhash is still valid is still the right way to chase a landing.

Don't re-sign to retry, though. A new signature is a different transaction, and it can land independently of the first.

## Related

* [Falcon native UDP](/falcon/udp)
* [Falcon tips & transaction requirements](/falcon/tips)
* [Falcon errors & retries](/falcon/errors)
