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

# Your First Payment

> An end-to-end walkthrough: register, fund, open a channel, sign a commitment, and settle.

This guide shows the smallest useful Ryvo integration: one payer, one payee, one token, one payment channel, and one settled `ryvo-cmt-v5` message.

The examples below mirror the current protocol tests. They use raw Anchor calls so you can see the exact accounts and signatures involved.

<Tip>
  If you don't need that level of control, the official [`@ryvonetwork/sdk`](/sdk) wraps every step on this page into one or two method calls. See the SDK [Recipes](/sdk/recipes) for the same flow in a dozen lines.
</Tip>

## What you will build

By the end of this guide, you will have:

1. Two registered participants.
2. One funded payer.
3. One open payment channel.
4. One signed unilateral commitment.
5. One successful direct settlement.

## Prerequisites

* Anchor workspace wired to the Ryvo program at `3UyUFeNsUYPpM6hMRf7H8wg3MKEXQ82rqnsXhZrUwgSD` on `devnet`
* Two keypairs with a small amount of SOL on devnet
* One allowlisted settlement token. Gateway-channel v1 uses official devnet USDC, mint `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`; resolve its `token_id` from `TokenRegistry` or `RYVO_PROTOCOL_DEVNET_USDC_TOKEN_ID`.

## 1. Create the program client

```ts theme={null}
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Ed25519Program, PublicKey, SystemProgram } from "@solana/web3.js";
import { RyvoProtocol } from "../target/types/ryvo_protocol";

const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);

const program = anchor.workspace.RyvoProtocol as Program<RyvoProtocol>;
```

## 2. Derive the PDAs

You will use deterministic PDA helpers throughout the protocol flow, so define these helpers once:

```ts theme={null}
const findParticipantPda = (owner: PublicKey) =>
  PublicKey.findProgramAddressSync(
    [Buffer.from("participant"), owner.toBytes()],
    program.programId
  )[0];

const findTokenRegistryPda = () =>
  PublicKey.findProgramAddressSync(
    [Buffer.from("token-registry")],
    program.programId
  )[0];

const findVaultTokenAccountPda = (tokenId: number) =>
  PublicKey.findProgramAddressSync(
    [
      Buffer.from("vault-token-account"),
      new anchor.BN(tokenId).toArrayLike(Buffer, "le", 2),
    ],
    program.programId
  )[0];

const findChannelPda = (payerId: number, payeeId: number, tokenId: number) =>
  PublicKey.findProgramAddressSync(
    [
      Buffer.from("channel-v2"),
      new Uint8Array(new Uint32Array([payerId]).buffer),
      new Uint8Array(new Uint32Array([payeeId]).buffer),
      new Uint8Array(new Uint16Array([tokenId]).buffer),
    ],
    program.programId
  )[0];
```

<Note>
  The channel PDA seed order is `payer_id (u32 LE) || payee_id (u32 LE) || token_id (u16 LE)` under the `channel-v2` prefix. If you derive in a different order, `settle_*` instructions will reject the account.
</Note>

## 3. Register both wallets

Each wallet registers once and keeps the same `participant_id` for the life of the deployment.

```ts theme={null}
await program.methods
  .initializeParticipant()
  .accounts({
    owner: alice.publicKey,
    feeRecipient,
  } as any)
  .signers([alice])
  .rpc();

await program.methods
  .initializeParticipant()
  .accounts({
    owner: bob.publicKey,
    feeRecipient,
  } as any)
  .signers([bob])
  .rpc();

const aliceParticipantPda = findParticipantPda(alice.publicKey);
const bobParticipantPda = findParticipantPda(bob.publicKey);

const aliceParticipant = await program.account.participantAccount.fetch(
  aliceParticipantPda
);
const bobParticipant = await program.account.participantAccount.fetch(
  bobParticipantPda
);
```

Read the assigned participant IDs:

```ts theme={null}
console.log(aliceParticipant.participantId);
console.log(bobParticipant.participantId);
```

## 4. Deposit the settlement token

Ryvo only settles allowlisted tokens. Once the payer has a normal SPL token account, deposit from that account into the protocol vault:

```ts theme={null}
await program.methods
  .deposit(tokenId, new anchor.BN(amount))
  .accounts({
    owner: alice.publicKey,
    participantAccount: aliceParticipantPda,
    ownerTokenAccount: aliceTokenAccount,
    vaultTokenAccount: findVaultTokenAccountPda(tokenId),
  } as any)
  .signers([alice])
  .rpc();
```

This moves tokens from the payer's token account into the protocol vault and credits the payer's `available_balance`.

## 5. Create the channel

One one-way payment channel exists for each `payer_id + payee_id + token_id` relationship. Channels are permanent.

```ts theme={null}
const channelPda = findChannelPda(
  aliceParticipant.participantId,
  bobParticipant.participantId,
  tokenId
);

await program.methods
  .createChannel(tokenId, null)
  .accounts({
    tokenRegistry: findTokenRegistryPda(),
    owner: alice.publicKey,
    payerAccount: aliceParticipantPda,
    payeeAccount: bobParticipantPda,
    payeeOwner: bob.publicKey,
    channelState: channelPda,
    systemProgram: SystemProgram.programId,
  } as any)
  .signers([alice, bob])
  .rpc();
```

The `payeeOwner` signer is only required when the payee's inbound-channel policy demands consent. Under `Permissionless`, channel creation does not require the payee to sign.

## 6. Optionally lock funds

Locked funds are optional. Use them when the payee wants guaranteed payment capacity on the channel.

```ts theme={null}
await program.methods
  .lockChannelFunds(tokenId, new anchor.BN(1_000_000))
  .accounts({
    owner: alice.publicKey,
    payerAccount: aliceParticipantPda,
    payeeAccount: bobParticipantPda,
    channelState: channelPda,
  } as any)
  .signers([alice])
  .rpc();
```

Settlement spends locked balance first, then shared participant balance.

## 7. Build the `ryvo-cmt-v5` message

`ryvo-cmt-v5` is cumulative. It does not say *"pay 25 again."* It says *"this channel is now authorized up to cumulative amount X."*

The helper below matches the real test suite:

```ts theme={null}
function encodeCompactU64(value: bigint): number[] {
  const bytes: number[] = [];
  let remaining = value;
  do {
    let byte = Number(remaining & 0x7fn);
    remaining >>= 7n;
    if (remaining > 0n) byte |= 0x80;
    bytes.push(byte);
  } while (remaining > 0n);
  return bytes;
}

function createCommitmentMessage(params: {
  payerId: number;
  payeeId: number;
  tokenId: number;
  committedAmount: anchor.BN;
  messageDomain: Buffer;
  // no delegated submitter field
}): Buffer {
  const flags = 0;

  const bodyParts = [
    Buffer.from(encodeCompactU64(BigInt(params.payerId))),
    Buffer.from(encodeCompactU64(BigInt(params.payeeId))),
    new anchor.BN(params.tokenId).toArrayLike(Buffer, "le", 2),
    Buffer.from(encodeCompactU64(BigInt(params.committedAmount.toString()))),
  ];

  return Buffer.concat([
    Buffer.from([0x01, 0x05]),
    params.messageDomain,
    Buffer.from([flags]),
    ...bodyParts,
  ]);
}
```

Build the first commitment:

```ts theme={null}
const channel = await program.account.channelState.fetch(channelPda);

const delta = new anchor.BN(250_000);
const committedAmount = channel.settledCumulative.add(delta);

const message = createCommitmentMessage({
  payerId: channel.payerId,
  payeeId: channel.payeeId,
  tokenId,
  committedAmount,
  messageDomain,
});
```

## 8. Add the Ed25519 verification instruction

Ryvo expects the signed message to be verified by Solana's Ed25519 program inside the same transaction:

```ts theme={null}
const ed25519Ix = Ed25519Program.createInstructionWithPrivateKey({
  privateKey: alice.secretKey,
  message,
});
```

The signer above must match the channel's current `authorized_signer`. This key signs new cumulative payment amounts for that channel. By default it is the payer wallet that created the channel; if the channel uses a different signing key, that key must sign here instead.

<Tip>
  `authorized_signer` signs the payment update. Settlement submitter logic stays outside the signed commitment body.
</Tip>

## 9. Submit `settle_individual`

The payee submits the settlement transaction:

```ts theme={null}
await program.methods
  .settleIndividual()
  .accounts({
    channelState: channelPda,
    payerAccount: aliceParticipantPda,
    payeeAccount: bobParticipantPda,
    submitter: bob.publicKey,
  } as any)
  .preInstructions([ed25519Ix])
  .signers([bob])
  .rpc();
```

The program verifies the Ed25519 instruction, parses the message, checks the `message_domain`, validates the canonical payer / payee / token / channel, then moves the delta between the old and new cumulative amounts.

## 10. Read the result

After settlement:

1. `channel.settled_cumulative` has advanced.
2. The payer balance has decreased by the delta.
3. The payee balance has increased by the same amount.
4. Any locked funds used by the settlement have been consumed first.

If an operator also needs to be paid, model that as a separate channel payment rather than as a fee field inside this message. See [Operator payment](/core-concepts/operator-hubs#operator-payment-is-separate).

That is the full direct settlement flow.

## Where to go next

<CardGroup cols={2}>
  <Card title="Bundle settlement" icon="layer-group" href="/settlement-modes/bundle-settlement">
    Settle many payers' commitments for one payee in a single transaction.
  </Card>

  <Card title="Cooperative clearing" icon="circle-nodes" href="/settlement-modes/clearing-rounds">
    Compress many payments across many participants into one round.
  </Card>

  <Card title="Message formats" icon="code" href="/reference/messages">
    The exact byte layout of `ryvo-cmt-v5` and cooperative round messages.
  </Card>
</CardGroup>
