> ## Documentation Index
> Fetch the complete documentation index at: https://nelo.udokaam.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Voucher format

> 202 bytes, version 1 — the wire contract between the phone and the chain.

A voucher is a fixed-layout, little-endian, 202-byte packet with no optional fields. Bytes `0..105` are the message the secure element signs.

## The layout

|     Off | Len | Field             | Type       | Notes                                                                  |
| ------: | --: | ----------------- | ---------- | ---------------------------------------------------------------------- |
|       0 |   1 | `version`         | `u8`       | `= 1`. Anything else is rejected outright                              |
|       1 |  32 | `vault`           | `Pubkey`   | Binds the voucher to one payer                                         |
|      33 |   8 | `seq`             | `u64`      | Monotonic per vault — the anti-replay handle                           |
|      41 |   8 | `amount`          | `u64`      | Token base units, USDC 6 dp                                            |
|      49 |   8 | `remaining_after` | `u64`      | Payer's claimed balance after — lets the merchant sanity-check offline |
|      57 |  32 | `merchant`        | `Pubkey`   | A voucher is not bearer; it names its payee                            |
|      89 |   8 | `expires_at`      | `i64`      | Bounds how long a signed voucher can sit unredeemed                    |
|      97 |   8 | `salt`            | `[u8; 8]`  | Per-voucher randomness                                                 |
| **105** |  64 | `signature`       | `[u8; 64]` | P-256 `r‖s`, **low-S**, from StrongBox                                 |
|     169 |  33 | `device_pubkey`   | `[u8; 33]` | SEC1 compressed — carried so the merchant can verify with no network   |

**Signed message: 105 bytes. Total packet: 202 bytes.**

## Why 202 is the budget

<Note>
  202 bytes fits an NDEF record for NFC **and** a QR code at version 10, error-correction level M — 213 bytes of capacity in byte mode, on a 57×57 grid that scans cleanly off a phone screen.
</Note>

That means the offline path is **not hard-blocked on NFC**, which matters because the low-cost Android handsets this product targets do not all have usable host card emulation. QR is the transport that cannot fail on hardware you do not control.

<Warning>
  Any field added later comes out of that budget. Growing past 213 bytes costs you QR version 10, and the fallback is a denser code that scans worse in a dim shop.
</Warning>

## The layout is a contract

The TypeScript in `@nelo/voucher` and the Rust in `programs/nelo_vault` must produce **byte-identical** signed messages, or every signature fails verification.

That is not left to careful reading. `packages/voucher/vectors/voucher-v1.json` is a frozen set of golden vectors:

* **Generated by the Rust side**, because the chain is authoritative.
* **Asserted by both sides** — `tests/vectors.rs` and `test/voucher.test.ts`.
* TypeScript also verifies signatures that Rust produced, so the check covers the crypto and not only the byte layout.

If the two ever drift, the vectors are where it surfaces — not in the field.

## Encoding

```ts theme={null}
import { signedMessage, encode, decode, verify } from "@nelo/voucher";

// The 105 bytes the secure element signs
const message = signedMessage({
  version: 1,
  vault,            // Uint8Array(32)
  seq: 5n,
  amount: 2_000_000n,        // $2.00 at 6 dp
  remainingAfter: 18_000_000n,
  merchant,         // Uint8Array(32)
  expiresAt: 1789000000n,
  salt,             // Uint8Array(8)
});

const signature = await sign("nelo-device", message);   // @nelo/attest, StrongBox
const packet = encode({ ...fields, signature, devicePubkey });   // 202 bytes
```

Decoding is total — a packet of the wrong length, or carrying an unknown version, is refused rather than partially parsed.

## `remaining_after` is a claim, not a fact

This is the field most likely to be misread.

`remaining_after` is what the **payer's device asserts** its balance will be once this voucher settles. It exists so a merchant with no network can sanity-check a voucher against the last vault balance they saw cached.

The program does **not** verify it. It cannot — it has no idea what other vouchers are in flight. What the program checks is the real constraint: `amount <= vault.balance` at redemption, and the replay window.

<Warning>
  A payer running a modified device can sign successive sequences that each carry a plausible `remaining_after` while together exceeding their collateral. The replay window stops the **same** sequence settling twice; it does not stop different sequences over-spending in aggregate.

  `report_conflict` only accepts two vouchers at the *same* sequence, so that pattern does not trigger the freeze either. **This is the exposure the insurance line prices** — see [the reserve model](/economics/reserve-model). Widening the conflict proof to accept an inconsistent-`remaining_after` pair would shorten the window a compromised device keeps trading in; it is an open design decision, not an oversight.
</Warning>

## Low-S, and why it bites

P-256 signatures have two valid forms for every signature: `(r, s)` and `(r, n − s)`. Android returns whichever it computes. The Solana precompile accepts only the **low-S** form.

The failure is nasty because it is intermittent and silent-looking: roughly half your signatures verify fine on the phone and are rejected on chain with no obvious pattern.

```ts theme={null}
// @nelo/voucher — every signing path must go through this
const raw = derToRawSignature(derFromAndroid);   // DER → r‖s, normalised low-S
```

Both the normalisation and the failure mode are pinned by tests that run without a handset.
